Чи завжди виконується блок finally?
Блок finally виконується завжди (за рідкіткими винятками -- див. Middle Level). Для початківців вважайте, що finally виконується гарантовано.
Junior Level
Базове правило
Блок finally виконується завжди (за рідкіткими винятками – див. Middle Level). Для початківців вважайте, що finally виконується гарантовано.
try {
System.out.println("try");
} finally {
System.out.println("finally"); // Виконається обов'язково
}
// Вивід: try \n finally
З винятком
try {
throw new RuntimeException("Error");
} finally {
System.out.println("Cleanup"); // Все одно виконається
}
// Вивід: Cleanup, потім виняток летить далі
Навіщо потрібен finally
Для звільнення ресурсів — закриття файлів, з’єднань, блокувань:
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// робота з файлом
} finally {
if (fis != null) fis.close(); // Закриється у будь-якому разі
}
У сучасному Java використовуйте try-with-resources — він робить це автоматично.
Middle Level
Коли НЕ використовувати finally
Для закриття ресурсів використовуйте try-with-resources замість finally з close(). finally використовуйте для: зняття блокувань (ReentrantLock.unlock()), відновлення стану (Thread.currentThread().interrupt()), логування.
Як це працює у байт-коді
У старих Java (до 6) використовувалася інструкція jsr (Jump to Subroutine). У сучасній Java компілятор копіює байт-код блоку finally у всі гілки завершення метода — після try, після кожного catch, і в неявний обробник винятків.
Це збільшує розмір метода, але робить потік управління лінійним.
Коли finally НЕ виконається
System.exit(n)— примусове завершення JVMRuntime.halt(n)— ще жорсткіше, навіть не запускає Shutdown Hooks- Нескінченний цикл у
try— потік ніколи не дійде до виходу - Deadlock — потік заблокований назавжди
- Kill -9 — сигнал
SIGKILLвбиває процес на рівні ОС - Daemon Threads — якщо залишилися лише демони, JVM завершується не дочекавшись
- Падіння JVM —
VirtualMachineErrorу момент переходу доfinally
Небезпека: перезапис return
int method() {
try { return 1; }
finally { return 2; }
}
// Поверне 2! finally перезаписує значення повернення
return у finally виконується ПІСЛЯ того, як значення вже було підготовлено у try. finally має останній шанс змінити результат. У байткоді: значення з try зберігається у тимчасову змінну, finally виконується, і якщо є свій return – він перезаписує результат.
Небезпека: проковтування винятків
try {
throw new RuntimeException("Original error");
} finally {
throw new RuntimeException("Finally error");
}
// Оригінальний виняток втрачено назавжди!
Senior Level
Shadowing — втрата Root Cause
Shadowing (затінення) – коли один виняток «ховає» інший. Root Cause (першопричина) – найперший виняток у ланцюжку, який спричинив усі інші.
Якщо у try виник виняток A, а у finally — виняток B, то A буде втрачено:
try {
// SQLException — БД недоступна
connection.executeQuery("...");
} finally {
// NPE — затьмарює SQLException
connection.close(); // connection == null
}
// У логах лише NPE, інформації про БД немає
Рішення: try-with-resources додає винятки з close() у список suppressed, а не затирає основний.
Правильний патерн з блокуваннями
Lock lock = new ReentrantLock();
lock.lock(); // Блокування ДО try
try {
// критична секція
} finally {
lock.unlock(); // Завжди звільняємо
}
Переконайтеся, що lock() був до try. Якщо lock() кине виняток, unlock() спробує звільнити незахоплене блокування → IllegalMonitorStateException.
Locking Pitfall у finally
Якщо всередині finally викликається метод, який може заблокуватися (запис у лог на переповнений диск), ви підвісите весь потік очищення ресурсів.
finally та try-with-resources
try (Resource r = new Resource()) {
// робота
} catch (Exception e) {
// ресурси ВЖЕ закриті тут
} finally {
// ресурси теж вже закриті
}
Блоки catch та finally, написані вручну, виконуються після автоматичного закриття ресурсів.
Діагностика
- Deadlock у finally — якщо
finallyвикликає блокуючий метод, потік зависне - Робіть код у
finallyмаксимально простим і швидким - Завжди захищайте
finallyвід нових винятків - Для аналізу використовуйте
javap -c— побачите, якfinallyскопійовано у всі гілки
🎯 Шпаргалка для співбесіди
Обов’язково знати:
finallyвиконується завжди (за рідкіткими винятками)- Коли finally НЕ виконається:
System.exit(),Runtime.halt(), нескінченний цикл, deadlock, kill -9, daemon threads, падіння JVM returnуfinallyперезаписує значення зtry— поверне значення з finally- Виняток у
finallyзатирає основний виняток зtry(shadowing) - Компілятор копіює байт-код
finallyу всі гілки — збільшує розмір метода - Для закриття ресурсів використовуйте try-with-resources, а не finally
- finally використовуйте для: зняття блокувань, відновлення стану, логування
- Блоки
catchтаfinallyвиконуються ПІСЛЯ автоматичного закриття ресурсів у TWR
Часті уточнюючі запитання:
- Чи може finally не виконатися? — Так: System.exit(), kill -9, нескінченний цикл, падіння JVM
- Що буде якщо return у finally? — Перезапише значення повернення з try — це баг
- Що таке shadowing винятків? — Виняток з finally затирає основний з try; рішення — try-with-resources з suppressed
- Правильний патерн для блокувань? —
lock.lock()ДО try,lock.unlock()у finally
Червоні прапорці (НЕ говорити):
- “Finally виконується абсолютно завжди” — ні, System.exit() та kill -9 зупиняють JVM
- “Я використовую return у finally для повернення дефолтного значення” — це антипатерн, перезаписує результат try
- “Ловлю виняток у finally і продовжую” — оригінальний виняток втрачено назавжди
Пов’язані теми:
- [[Що таке try-with-resources]]
- [[Що таке suppressed exceptions]]
- [[Які вимоги до ресурсів в try-with-resources]]
- [[Що таке exception chaining]]
- [[Як правильно логувати винятки]]