Гарантируется ли выполнение блока 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]]
- [[Как правильно логировать исключения]]