Що таке unchecked виняток (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).