Які винятки потрібно завжди обробляти
Відповідь на це запитання залежить від того, хто визначає обов'язковість — компілятор Java (javac) чи архітектура надійності застосунку.
🟢 Junior Level
Відповідь на це запитання залежить від того, хто визначає обов’язковість — компілятор Java (javac) чи архітектура надійності застосунку.
1. З погляду компілятора Java:
Обов’язковими до обробки є виключно Checked Exceptions (перевірювані винятки):
- Це всі класи, що успадковуються від
java.lang.Exception, окрім гілкиjava.lang.RuntimeException. - Якщо метод викликає код, який викидає checked-виняток, розробник зобов’язаний обрати одну з двох дій:
- Обернути виклик у конструкцію
try-catch. - Оголосити перевірюваний виняток у сигнатурі методу через ключове слово
throws.
- Обернути виклик у конструкцію
Якщо жодної з дій не виконано, вихідний код не скомпілюється:
// ПОМИЛКА КОМПІЛЯЦІЇ: Unhandled exception: java.io.IOException
public void readFile() {
Files.readAllBytes(Path.of("config.json"));
}
2. Зведена таблиця обов’язковості обробки
| Категорія винятків | Приклади | Обов’язково компілятором? | Обов’язково в архітектурі? |
|---|---|---|---|
| Checked Exceptions | IOException, SQLException, InterruptedException |
✅ Так (суворо) | ✅ Так (локально або трансляція) |
| Unchecked Exceptions | NullPointerException, IllegalArgumentException |
❌ Ні | 🟡 Так (на межах шарів та потоків) |
| Errors | OutOfMemoryError, StackOverflowError |
❌ Ні | 🛑 Заборонено перехоплювати |
🟡 Middle Level
Обов’язкова обробка на межах системи (System Boundaries)
Хоча компілятор не змушує писати try-catch для RuntimeException, у реальному промисловому застосунку необроблений виняток:
- Завершить потік виконання (Thread death).
- Спричинить витік деталей реалізації клієнту (Internal Server Error 500 зі стек-трейсом).
- Призведе до зависання розподіленої транзакції або повідомлення в черзі брокера (Kafka / RabbitMQ).
Тому на периметрі системи (межі шарів) обробка unchecked-винятків є архітектурно обов’язковою:
- У REST API: Верхньорівневий
@RestControllerAdviceзобов’язаний перехоплювати всі непередбаченіExceptionі повертати клієнту знеособлений RFC 9457ProblemDetail. - У Consumer Kafka / RabbitMQ: Обробник черги зобов’язаний перехоплювати помилки парсингу та бізнес-збої, спрямовуючи пошкоджені повідомлення в Dead Letter Queue (DLQ).
Чому перехоплення Error та Throwable категорично не рекомендується?
Розробники-початківці часто пишуть:
// ❌ НЕБЕЗПЕЧНИЙ АНТИПАТЕРН:
try {
doSomeHeavyComputation();
} catch (Throwable t) { // Перехоплює Error, OutOfMemoryError, ThreadDeath!
log.error("Error occurred", t);
}
Чим це загрожує:
java.lang.Error(зокремаOutOfMemoryError,InternalError,UnknownError) сигналізує про катастрофічну відмову середовища виконання JVM.- У разі виникнення
OutOfMemoryErrorпам’ять купи (Heap) вичерпана, внутрішні структури JVM можуть бути пошкоджені, а монітори синхронізації розблоковані в некоректному стані. - Спроба «проковтнути»
OutOfMemoryErrorі продовжити роботу програми переводить сервіс у стан «зомбі»: запити зависають, блокування не звільняються, інстанс не відповідає на Health Check. Єдина правильна дія приError— дати процесу впасти та дозволити Kubernetes / Systemd перезапустити под.
🔴 Senior Level
Необроблені винятки на межі багатопоточності
Якщо всередині фонового потоку (Worker Thread) викидається unchecked-виняток, поведінка залежить від механізму запуску:
new Thread(runnable).start(): Потік негайно завершує роботу. За замовчуванням стек-трейс друкується вSystem.err, а сам застосунок може навіть не зафіксувати збій у лог-файлі.ExecutorService.execute(Runnable): Потік завершується аварійно, пул потоків створює новий потік замість загиблого.ExecutorService.submit(Callable): Виняток перехоплюється реалізацієюFutureTaskі зберігається всередині. Якщо викликаючий код не викликав методfuture.get(), виняток безслідно розчиняється, і ніхто не дізнається про аварію.
Production рішення: Встановлення UncaughtExceptionHandler
Для запобігання непомітній загибелі потоків на рівні ініціалізації застосунку реєструється обробник:
package com.example.init;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class GlobalExceptionHandlerInitializer implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandlerInitializer.class);
@Override
public void run(String... args) {
// Перехоплює всі необроблені винятки в будь-яких потоках JVM
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
if (throwable instanceof Error) {
log.error("FATAL: JVM Error in thread [{}]. Initiating emergency shutdown.",
thread.getName(), throwable);
// Завершуємо процес при фатальних системних помилках
System.exit(1);
} else {
log.error("CRITICAL: Uncaught exception in thread [{}]",
thread.getName(), throwable);
}
});
}
}
Особливий статус InterruptedException: контракт кооперативної багатопоточності
InterruptedException — це перевірюваний виняток, що вимагає обов’язкової архітектурної дисципліни.
Потік у Java не можна безпечно «вбити» ззовні (метод Thread.stop() оголошено deprecated і видалено). Зупинка потоків є виключно кооперативною.
Коли потік спить у Thread.sleep(), чекає на моніторі wait() або блокується в черзі BlockingQueue.take(), виклик thread.interrupt() викидає InterruptedException і скидає прапорець переривання потоку в false.
Правило обробки:
public void runWorkerLoop() {
while (!Thread.currentThread().isInterrupted()) {
try {
Task task = taskQueue.take(); // Викидає InterruptedException
task.execute();
} catch (InterruptedException ex) {
// ОБОВ'ЯЗКОВО: відновити перерваний статус потоку!
Thread.currentThread().interrupt();
log.info("Worker thread interrupted, exiting gracefully...");
break; // Завершуємо робочий цикл
}
}
}
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
- З погляду компілятора Java, обов’язковими до обробки є лише Checked Exceptions (
IOException,SQLException,InterruptedException). Компілятор вимагає або обернути їх уtry-catch, або задекларувати в секціїthrows.- З погляду архітектури,
RuntimeExceptionобов’язково перехоплюються на межах системи (у@RestControllerAdvice, обробниках черг Kafka, фільтрах безпеки) для запобігання падінню потоків і формування зрозумілих відповідей клієнту.- Системні помилки
Errorперехоплювати категорично заборонено: вони свідчать про фізичну непрацездатність JVM (OutOfMemoryError), і процес має бути перезапущений оркестратором.- На межах багатопоточності обов’язковим є встановлення
Thread.setDefaultUncaughtExceptionHandler.
4 каверзних питання з відповідями
-
Що станеться, якщо в завданні, переданому в
executorService.submit(runnable), відбудеться ділення на нуль (ArithmeticException)? Відповідь: Пул потоків перехопитьArithmeticExceptionі збереже його всередині повернутого об’єктаFuture. Жодних логів у консоль виведено не буде. Якщо потік, що викликав завдання, не викличеfuture.get(), виняток буде безслідно проковтнутий. Якщо викликатиfuture.get(), він буде викинутий загорнутим уExecutionException. -
Чому перехоплення
catch (Throwable t)вважається небезпечним антипатерном? Відповідь: КласThrowableє спільним предком дляExceptionтаError. ПерехоплюючиThrowable, блокcatchзаблокує природне падіння віртуальної машини приOutOfMemoryErrorабоStackOverflowError. Застосунок залишиться в напівзруйнованому «зомбі»-стані з вичерпаною пам’яттю або пошкодженими блокуваннями. -
Що відбувається з прапорцем переривання потоку у разі виникнення
InterruptedExceptionі як правильно вчинити? Відповідь: Механізм JVM під час викиданняInterruptedExceptionавтоматично скидає прапорець переривання вfalse. Якщо розробник просто залогує помилку і продовжить роботу, зовнішній сигнал про зупинку сервісу буде втрачено. Обробник зобов’язаний або прокинути виняток далі, або відновити статус викликомThread.currentThread().interrupt(). -
Як компілятор Java перевіряє обов’язковість обробки checked-винятків усередині багатомодульних проєктів? Відповідь: На основі атрибута байткоду
Exceptionsу.classфайлах залежностей. Компіляторjavacбудує граф викликів і перевіряє, що кожен викликаний метод з оголошенимthrowsабо укладений у блокtryз відповіднимcatch, або поточний метод сам містить цей виняток (чи його предка) у своємуthrows.
Червоні прапорці (чого категорично не можна говорити)
- ❌ «Потрібно завжди загортати весь код методу в catch (Throwable t), щоб програма ніколи не падала» (груба помилка: це ламає моніторинг і призводить до зомбі-процесів при OutOfMemoryError).
- ❌ «Компілятор змушує обробляти NullPointerException» (NPE — це unchecked виняток, компілятор його не контролює).
- ❌ «Якщо потік перервали через InterruptedException, можна просто продовжити цикл» (порушує кооперативну багатопоточність і перешкоджає graceful shutdown).
- ❌ «У мікросервісах checked-винятки зобов’язані доходити прямо до фронтенду» (checked-винятки інфраструктури зобов’язані транслюватися в доменні помилки на межі шарів).