Вопрос 8 · Раздел 7

Гарантируется ли выполнение блока finally?

Блок finally выполняется всегда (за редкими исключениями -- см. Middle Level). Для начинающих считайте, что finally выполняется гарантированно.

Версии по языкам: English Russian Ukrainian

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 НЕ выполнится

  1. System.exit(n) — принудительное завершение JVM
  2. Runtime.halt(n) — ещё жёстче, даже не запускает Shutdown Hooks
  3. Бесконечный цикл в try — поток никогда не дойдёт до выхода
  4. Deadlock — поток заблокирован навсегда
  5. Kill -9 — сигнал SIGKILL убивает процесс на уровне ОС
  6. Daemon Threads — если остались только демоны, JVM завершается не дожидаясь
  7. Падение JVMVirtualMachineError в момент перехода к 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]]
  • [[Как правильно логировать исключения]]