Что такое unchecked exception (Runtime Exception)
В иерархии типов непроверяемыми являются класс java.lang.RuntimeException и все его прямые и косвенные потомки (а также ветка системных ошибок java.lang.Error).
🟢 Junior Level
Unchecked exception (непроверяемое исключение) — это тип исключительной ситуации в Java, который компилятор javac не требует принудительно оборачивать в try-catch или объявлять в сигнатуре метода через ключевое слово throws.
В иерархии типов непроверяемыми являются класс java.lang.RuntimeException и все его прямые и косвенные потомки (а также ветка системных ошибок java.lang.Error).
Причины возникновения unchecked-исключений
- Ошибки программирования (баги в коде):
NullPointerException(NPE): Попытка вызвать метод или обратиться к полю по ссылке, равнойnull.IndexOutOfBoundsException: Обращение к несуществующему индексу массива или списка (list.get(10)при размере 5).ClassCastException: Некорректное приведение типов в runtime.ArithmeticException: Деление целого числа на ноль (10 / 0).
- Нарушение контракта вызова метода:
IllegalArgumentException: Передан некорректный аргумент (например, отрицательный возраст пользователя).IllegalStateException: Метод вызван в неподходящий момент жизненного цикла объекта (например, попытка чтения из закрытого потока).UnsupportedOperationException: Попытка модифицировать неизменяемую коллекцию (List.of().add(...)).
Базовый пример в коде
import java.util.Objects;
public class UncheckedBasics {
// Метод НЕ обязан объявлять throws IllegalArgumentException
public void setDiscountPercent(int percent) {
if (percent < 0 || percent > 100) {
throw new IllegalArgumentException("Discount must be between 0 and 100%");
}
// бизнес-логика...
}
public static void main(String[] args) {
UncheckedBasics service = new UncheckedBasics();
// Компилятор не заставляет писать try-catch
service.setDiscountPercent(150); // Вызовет исключение в Runtime
}
}
🟡 Middle Level
Доменные Unchecked Exceptions и принцип Fail-Fast
В современной разработке unchecked-исключения делятся на две категории:
- Технические дефекты кода (
NullPointerException,IndexOutOfBoundsException) — их не нужно ловить черезtry-catch, их нужно исправлять в коде (багфиксы). - Доменные бизнес-исключения (
UserNotFoundException,OrderAlreadyPaidException) — они сознательно наследуются отRuntimeExceptionдля информирования о штатных отказах бизнес-логики.
Принцип Fail-Fast (Быстрый отказ)
Код должен проверять входные параметры на самой ранней стадии выполнения и немедленно выбрасывать unchecked-исключение, не допуская проникновения некорректного состояния вглубь системы:
public Order processOrder(Long userId, OrderRequest request) {
Objects.requireNonNull(userId, "User ID must not be null");
Objects.requireNonNull(request, "Order request must not be null");
if (request.items().isEmpty()) {
throw new IllegalArgumentException("Order must contain at least one item");
}
return orderRepository.save(new Order(userId, request.items()));
}
Архитектура Spring Boot: централизованная обработка исключений
Благодаря тому, что RuntimeException свободно поднимается вверх по стеку вызовов через все слои (Repository -> Service -> Controller), в Spring приложениях исчезает необходимость в громоздких блоках try-catch.
Все доменные ошибки перехватываются единым централизованным компонентом @RestControllerAdvice:
package com.example.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.net.URI;
import java.time.Instant;
@RestControllerAdvice
public class GlobalErrorHandlingAdvice {
// Обработка бизнес-ошибки: 404 Not Found
@ExceptionHandler(EntityNotFoundException.class)
public ProblemDetail handleNotFound(EntityNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Resource Not Found");
problem.setType(URI.create("https://api.example.com/errors/not-found"));
problem.setProperty("timestamp", Instant.now());
return problem;
}
// Обработка невалидных аргументов: 400 Bad Request
@ExceptionHandler(IllegalArgumentException.class)
public ProblemDetail handleBadRequest(IllegalArgumentException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
problem.setTitle("Invalid Request Argument");
problem.setProperty("timestamp", Instant.now());
return problem;
}
}
🔴 Senior Level
Антипаттерн “Exceptions as Control Flow” (Исключения для логики)
Использование исключений для управления потоком выполнения программы является грубейшим антипаттерном:
// ❌ АНТИПАТТЕРН: Исключение как ветвление логики (Control Flow)
public boolean userExists(Long userId) {
try {
userService.findById(userId); // Выбрасывает UserNotFoundException
return true;
} catch (UserNotFoundException ex) {
return false; // Катастрофическое падение производительности!
}
}
// ✅ ПРАВИЛЬНО: Использование Optional или boolean метода
public boolean userExists(Long userId) {
return userRepository.existsById(userId);
}
Почему это критично в Highload:
- Вызов метода
existsById()— это быстрая проверка условия. - Выбрасывание
UserNotFoundExceptionвызывает нативный методfillInStackTrace(), который производит дорогостоящую паузу потока и обход фреймов стека вызовов. Под нагрузкой в 50 000 RPS такой код приводит к 100% утилизации CPU и деградации приложения.
Опасность необработанных RuntimeException в пулах потоков
В многопоточном программировании unchecked-исключения таят скрытую опасность:
- Если поток в
ExecutorService.submit(Callable)падает сRuntimeException, исключение перехватывается фреймворком и «засыпает» внутри объектаFuture. Если разработчик не вызоветfuture.get(), ошибка будет проглочена бесследно, и задача молча пропадет. - В
ScheduledExecutorServiceнеобработанныйRuntimeExceptionвнутри периодической задачи приводит к полной и безвозвратной остановке выполнения всех будущих итераций этой задачи без каких-либо логов!
Решение: Установка UncaughtExceptionHandler:
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
log.error("CRITICAL: Uncaught exception in thread: {}", thread.getName(), throwable);
});
Stackless Runtime Exceptions для критичных путей
Для высоконагруженных шлюзов и валидаторов, где исключение используется строго как аварийный сигнал выхода из транзакции, создают экземпляры без стек-трейса:
package com.example.exception;
public class FastValidationException extends RuntimeException {
// Статический синглтон-экземпляр без оверхеда на память и процессор
public static final FastValidationException TOKEN_EXPIRED =
new FastValidationException("Security token expired");
public FastValidationException(String message) {
// writableStackTrace = false исключает обход стека JVM
super(message, null, false, false);
}
}
🎯 Шпаргалка для интервью
30-секундный ответ
Unchecked exception (непроверяемое исключение) — это исключение, наследующееся от
java.lang.RuntimeException(илиError). Компиляторjavacне принуждает разработчика оборачивать его вtry-catchили объявлять в сигнатуре метода (throws).Unchecked-исключения делятся на:
- Баги в коде (
NullPointerException,IndexOutOfBoundsException), которые не надо ловить, а нужно исправлять.- Доменные исключения (
UserNotFoundException,ValidationException), которые сигнализируют об отказах бизнес-логики.Вся современная разработка (Spring Boot, Hibernate, Reactive) построена на unchecked-исключениях, так как они не загрязняют сигнатуры методов, совместимы со Stream API/лямбдами и централизованно обрабатываются через
@RestControllerAdvice.
4 каверзных вопроса с ответами
-
Что произойдет, если в периодической задаче
ScheduledExecutorService.scheduleAtFixedRate()выбросится unchecked-исключениеRuntimeException? Ответ: Периодическое выполнение задачи навсегда прекратится. Планировщик подавляет исключение, помечает задачу как завершенную с ошибкой и больше никогда не запустит следующие итерации. Чтобы этого избежать, тело задачи обязательно оборачивают в верхнеуровневыйtry-catch (Throwable t)с логированием. -
Почему использование исключений для управления бизнес-логикой (Exceptions as Control Flow) является серьезным антипаттерном? Ответ: Во-первых, это делает код нечитаемым (нарушается предсказуемость потока управления). Во-вторых, это приводит к катастрофическому падению производительности: генерация стек-трейса в
fillInStackTrace()при создании каждогоThrowableобходится в сотни раз дороже, чем стандартная проверка черезifили возвратOptional. -
Нужно ли перехватывать базовые исключения вроде NullPointerException в блоке try-catch? Ответ: Нет, перехватывать
NullPointerExceptionилиIndexOutOfBoundsExceptionв кодовой базе считается антипаттерном. Такие исключения свидетельствуют о наличии логической ошибки (бага) в коде, и их нужно устранять предварительной валидацией входных данных (Objects.requireNonNull,@NotNull, проверка длины) или использованиемOptional. -
Чем конструктор
Throwable(message, cause, enableSuppression, writableStackTrace)помогает в оптимизации Highload-систем? Ответ: ПередачаwritableStackTrace = falseполностью отключает вызов тяжелого нативного методаfillInStackTrace(). Исключение создается со скоростью обычного Java-объекта без обхода фреймов стека памяти, что исключает просадки CPU при высоких RPS.
Красные флаги (чего категорически нельзя говорить)
- ❌ «Unchecked исключения никогда нельзя ловить через try-catch» (их можно и нужно перехватывать, например, централизованно в
@RestControllerAdviceили на границах системных потоков). - ❌ «Если объект может быть null, нужно просто обернуть его вызов в try-catch (NullPointerException e)» (грубейший антипаттерн; проверять null нужно через
Objects.requireNonNullили условия). - ❌ «RuntimeException и Error — это одно и то же» (
Errorсигнализирует о неустранимых сбоях JVM вродеOutOfMemoryError, аRuntimeException— об ошибках уровня приложения). - ❌ «Spring заставляет объявлять throws для всех сервисных методов» (наоборот, Spring полностью построен на иерархии
RuntimeException).