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

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

В стандартных условиях выполнения программы блок finally выполняется всегда — вне зависимости от того, завершился ли блок try успешно, было ли выброшено исключение или сработал...


🟢 Junior Level

В стандартных условиях выполнения программы блок finally выполняется всегда — вне зависимости от того, завершился ли блок try успешно, было ли выброшено исключение или сработал оператор return.

Основное назначение finally — гарантированное освобождение ресурсов (закрытие сокетов, снятие блокировок Lock.unlock(), очистка временных файлов).

public class FinallyBasics {

    public static String testFinally() {
        try {
            System.out.println("1. Внутри try");
            return "RESULT_FROM_TRY";
        } catch (Exception e) {
            System.out.println("2. Внутри catch");
            return "RESULT_FROM_CATCH";
        } finally {
            System.out.println("3. Внутри finally (выполняется перед возвратом!)");
        }
    }

    public static void main(String[] args) {
        String result = testFinally();
        System.out.println("4. Получено значение: " + result);
    }
}

Вывод в консоль:

1. Внутри try
3. Внутри finally (выполняется перед возвратом!)
4. Получено значение: RESULT_FROM_TRY

🟡 Middle Level

7 сценариев, когда блок finally НЕ выполнится

Утверждение «finally выполняется абсолютно всегда» является ошибочным. Существует 7 конкретных ситуаций, когда блок finally не будет вызван:

  1. System.exit(status) или Runtime.getRuntime().exit(status): Инициирует завершение работы JVM. Потоки приложения мгновенно останавливаются, запускаются только зарегистрированные Shutdown Hooks.
  2. Runtime.getRuntime().halt(status): Мгновенная жесткая остановка JVM без выполнения даже Shutdown Hooks и финализаторов.
  3. Завершение процесса операционной системой: Отправка сигнала SIGKILL (kill -9 <pid>), аварийное падение контейнера Docker по OOM Killer, сбой питания сервера или нажатие кнопки Stop в IDE.
  4. Потоки-демоны (Daemon Threads): Если поток помечен как демон (thread.setDaemon(true)), и все пользовательские потоки (non-daemon) завершили работу, JVM немедленно завершает процесс, не дожидаясь исполнения блоков finally в демонах!
  5. Зависание потока в блоке try: Бесконечный цикл (while(true)), взаимная блокировка потоков (Deadlock) или вечное блокирующее чтение из сокета без таймаута (Socket.read()). Поток физически не доходит до точки выхода из try.
  6. Крах самой виртуальной машины (Fatal JVM Crash): Сбой в нативном коде JNI (Segmentation Fault, SIGSEGV) или критический сбой ядра HotSpot JVM.
  7. Аппаратные ошибки питания и сбои CPU/RAM.

Опасность: перехват и затирание return

Если в блоке finally написать явный оператор return, он перезапишет результат возврата из блока try:

public static int dangerousReturn() {
    try {
        return 10;
    } finally {
        return 20; // Перезаписывает значение: метод вернет 20!
    }
}

Катастрофическая опасность: потеря исключения (Exception Swallowing)

Если блок try выбросил критическое исключение, а внутри блока finally также возникло исключение или вызван return, исходное исключение бесследно исчезает:

public static void swallowedException() {
    try {
        throw new IllegalStateException("CRITICAL_DATABASE_CRASH");
    } finally {
        return; // Исключение CRITICAL_DATABASE_CRASH подавлено и проглочено навсегда!
    }
}

Правило чистой архитектуры: Никогда не используйте операторы return, break или continue внутри блока finally. Статический анализатор SonarQube помечает return в finally как критический баг.


🔴 Senior Level

Реализация finally в байткоде JVM (JSR vs Inlining)

В ранних версиях Java (до Java 6) компилятор использовал специальные инструкции байткода jsr (Jump to Subroutine) и ret для перехода к подпрограмме finally.

Начиная с Java 6, ради оптимизации верификатора байткода и JIT-компиляции архитектура изменилась:

  • Компилятор javac дублирует (инлайнит) байткод блока finally во все возможные точки выхода из метода:
    1. В конец блока try (перед ireturn/return).
    2. В конец каждого блока catch.
    3. В специальный сгенерированный скрытый блок catch (any) в таблице исключений метода (Exception_table), перехватывающий любые непредвиденные Throwable для гарантированного выполнения кода finally с последующим перевыбросом через athrow.

Канонический паттерн работы с ReentrantLock

Классический вопрос на Senior-интервью: «Где должен располагаться вызов lock.lock() — внутри try или до него?»

// ✅ КАНОНИЧЕСКИЙ ПАТТЕРН:
Lock lock = new ReentrantLock();
lock.lock(); // 1. Захват блокировки СТРОГО ДО блока try!
try {
    // 2. Критическая секция бизнес-логики
    doBusinessWork();
} finally {
    lock.unlock(); // 3. Гарантированное освобождение в finally
}

Почему lock.lock() категорически нельзя помещать внутри try:

// ❌ ГРУБАЯ ОШИБКА:
Lock lock = new ReentrantLock();
try {
    lock.lock(); // Если здесь выбросится исключение (например, OOM или ThreadDeath)...
    doBusinessWork();
} finally {
    lock.unlock(); // ...finally попытается освободить НЕЗАХВАЧЕННЫЙ lock!
                   // Выбросит IllegalMonitorStateException, затерев оригинальный сбой!
}

Порядок выполнения в Try-with-resources с явными catch и finally

В конструкции try-with-resources автоматическое закрытие ресурсов происходит ДО входа в явные блоки catch и finally:

package com.example.resource;

public class ResourceOrderDemo {

    static class Door implements AutoCloseable {
        @Override
        public void close() {
            System.out.println("2. AutoCloseable.close() вызван автоматически");
        }
    }

    public static void main(String[] args) {
        try (Door door = new Door()) {
            System.out.println("1. Тело блока try");
            throw new RuntimeException("Ошибка в try");
        } catch (Exception e) {
            System.out.println("3. Блок catch: " + e.getMessage());
        } finally {
            System.out.println("4. Блок finally выполняется в самом конце");
        }
    }
}

Вывод:

1. Тело блока try
2. AutoCloseable.close() вызван автоматически
3. Блок catch: Ошибка в try
4. Блок finally выполняется в самом конце

🎯 Шпаргалка для интервью

30-секундный ответ

В штатных условиях выполнения программы блок finally выполняется всегда — как при успешном завершении try, так и при возникновении исключений или вызове return.

Однако выполнение НЕ гарантируется в случаях:

  1. Принудительного завершения JVM (System.exit(), Runtime.getRuntime().halt()).
  2. Убийства процесса операционной системой (kill -9, SIGKILL, сбой питания).
  3. Завершения приложения при активных потоках-демонах (Daemon Threads).
  4. Зависания потока внутри try (Deadlock, бесконечный цикл).
  5. Фатального краха среды (SIGSEGV в JNI, VirtualMachineError).

В блоке finally категорически запрещено использовать операторы return и выбрасывать необработанные исключения, так как они безвозвратно затирают исходное исключение из блока try. Для захвата Lock вызов lock.lock() обязан выполняться строго до блока try.

4 каверзных вопроса с ответами

  1. Что произойдет, если в потоке-демоне (isDaemon() == true) выполняется долгая операция внутри блока try, а все пользовательские потоки завершили работу? Ответ: JVM завершит процесс немедленно. Поток-демон будет остановлен операционной системой в произвольной точке выполнения, и код в его блоке finally не выполнится. Поэтому в демонах нельзя размещать критическую логику сохранения данных или очистки внешних ресурсов.

  2. Что вернет метод, если в блоке try написано return 1;, а в блоке finally написано return 2;? Ответ: Метод вернет 2. Значение, подготовленное в блоке try, помещается в локальную переменную, после чего управление передается в байткод блока finally. Оператор return в finally перезаписывает возвращаемое значение в операндном стеке, затирая исходный результат.

  3. Что произойдет, если в блоке try выброшено SQLException, а в блоке finally выброшено NullPointerException? Ответ: Исходное исключение SQLException будет безвозвратно потеряно (Masked / Shadowed). Клиент получит только NullPointerException из блока finally. Именно для устранения этой проблемы в Java 7 был внедрен синтаксис try-with-resources с механизмом Suppressed Exceptions.

  4. В каком порядке вызываются методы при использовании try-with-resources с блоками catch и finally? Ответ: Сначала выполняется код блока try. Затем автоматически вызываются методы close() у всех объявленных в скобках ресурсов (в порядке, обратном их объявлению). Только после полного закрытия всех ресурсов управление передается в явный блок catch (если возникла ошибка) и затем в финальный блок finally.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Блок finally выполняется абсолютно всегда в 100% случаев без исключений» (грубая ошибка; System.exit(), SIGKILL и демоны останавливают выполнение).
  • ❌ «Использовать return в finally — хорошая практика для задания дефолтного значения» (критический антипаттерн, приводящий к затиранию ошибок и непредсказуемым багам).
  • ❌ «Вызов lock.lock() нужно помещать внутрь блока try» (при сбое во время лока unlock() в finally упадет с IllegalMonitorStateException).
  • ❌ «В try-with-resources блок finally выполняется до закрытия ресурсов» (наоборот, автоматический close() вызывается строго до явных catch и finally).

Связанные темы