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

Что произойдёт, если в блоке finally тоже возникнет исключение

Если в блоке finally возникнет исключение, произойдет эффект маскирования исключения (Exception Masking / Shadowing):


🟢 Junior Level

Краткий ответ для собеседования (30 секунд)

Если в блоке finally возникнет исключение, произойдет эффект маскирования исключения (Exception Masking / Shadowing):

  1. Исключение из блока finally полностью вытеснит первичное исключение, возникшее в блоке try или catch.
  2. Первичное исключение будет безвозвратно потеряно: оно не попадет в лог, не будет передано вызывающему коду и удалится сборщиком мусора.
  3. Выполнение блока finally немедленно прервется в точке выброса ошибки. Если после этого следовало закрытие других ресурсов, они не будут закрыты, что приведет к утечке системных дескрипторов (Resource Leak).

Для предотвращения потери ошибок в современном коде вместо блоков finally всегда используют try-with-resources, который сохраняет первичную ошибку, а вторичные добавляет как подавленные (Suppressed Exceptions).


Наглядный пример вытеснения ошибки

public class FinallyMaskingDemo {
    public static void main(String[] args) {
        try {
            processData();
        } catch (Exception e) {
            // В лог попадет ТОЛЬКО NullPointerException из блока finally!
            // Первичный критический сбой DatabaseTimeoutException полностью стерт!
            System.err.println("Caught exception: " + e.getClass().getSimpleName() + " -> " + e.getMessage());
        }
    }

    public static void processData() {
        Connection conn = null;
        try {
            // 1. Возникает критическая ошибка бизнес-слоя
            throw new IllegalStateException("Database query timeout (Primary error)");
        } finally {
            // 2. Ошибка при закрытии недоинициализированного ресурса
            conn.close(); // conn == null -> выбрасывает NullPointerException!
        }
    }

    interface Connection { void close(); }
}

Вывод программы:

Caught exception: NullPointerException -> Cannot invoke "FinallyMaskingDemo$Connection.close()" because "conn" is null

Инженер будет расследовать NullPointerException, даже не подозревая, что настоящая проблема заключалась в таймауте базы данных.


🟡 Middle Level

Пошаговый механизм на уровне JVM

Спецификация виртуальной машины Java (JVM Specification, §3.13) определяет строгий порядок обработки аварийных выходов:

  1. Фаза Try: Инструкция athrow инициирует раскрутку стека. JVM сохраняет ссылку на первичное исключение E1 в локальном слоте стекового фрейма (Pending Exception Slot).
  2. Переход в Finally: Перед тем как покинуть метод, управление принудительно передается на стартовый адрес блока инструкций finally.
  3. Фаза Finally: Внутри блока finally происходит сбой и выполняется новая инструкция athrow с объектом E2.
  4. Перезапись указателя: Виртуальная машина перезаписывает активное исключение во фрейме: указатель на E1 заменяется на E2. Ссылка на объект E1 утрачивается навсегда.
  5. Раскрутка стека: Управление передается вверх по стеку вызовов уже с исключением E2.

Антипаттерн “Return in Finally” (Подавление через возврат)

Еще более коварным проявлением эффекта вытеснения является оператор return внутри finally:

public class ReturnInFinallyTrap {
    public static int calculate() {
        try {
            throw new RuntimeException("Fatal business error!");
        } finally {
            return 42; // ❌ КАТАСТРОФА: исключение тихо проглатывается!
        }
    }

    public static void main(String[] args) {
        int result = calculate();
        System.out.println("Result: " + result); // Выведет 42, ошибки нет!
    }
}

Инструкция байткода ireturn в блоке finally завершает фрейм метода в штатном режиме. Любое активное исключение из блока try молча уничтожается. По этой причине правило SonarQube java:S1143 классифицирует return в блоке finally как критический баг (Blocker).


Каскадная утечка ресурсов (Cascading Resource Leak)

Классический код закрытия нескольких ресурсов в одном блоке finally без try-with-resources:

// ❌ АНТИПАТТЕРН ДО JAVA 7:
finally {
    in.close();  // Если in.close() выбросит IOException...
    out.close(); // ...эта строка НИКОГДА НЕ ВЫПОЛНИТСЯ! Ресурс out утечет!
}

// Ручное исправление до Java 7 (громоздкий Boilerplate):
finally {
    try {
        if (in != null) in.close();
    } catch (IOException e) {
        log.warn("Failed to close in", e);
    }
    try {
        if (out != null) out.close();
    } catch (IOException e) {
        log.warn("Failed to close out", e);
    }
}

🔴 Senior Level

Как javac компилирует try-finally в современном байткоде

До Java 6 компилятор генерировал инструкции подпрограмм jsr (jump subroutine) и ret. Начиная с Java 6 (введение StackMapTable для быстрой однопроходной верификации типов), инструкции jsr/ret были запрещены.

В современном байткоде компилятор дублирует байткод блока finally для всех возможных путей выхода из метода:

  1. Копия finally для нормального завершения блока try.
  2. Копия finally для каждой секции catch.
  3. Копия finally внутри специального служебного обработчика любого исключения (any), генерируемого в Exception_table.
Exception table:
   from    to  target type
       0     5    10   Class java/lang/Exception
       0     5    20   any
      10    15    20   any

Если секция any при выполнении своего тела finally сама выбрасывает исключение, исполнение метода аварийно прерывается инструкцией athrow, не доходя до восстановления первичного исключения.


Архитектурное решение: Suppressed Exceptions в Try-With-Resources

Для устранения маскирования в Java 7 был представлен механизм подавленных исключений (Suppressed Exceptions). В конструкции try-with-resources:

  • Исключение из тела try всегда признается главным (Primary).
  • Если во время вызова close() также возникает исключение, компилятор перехватывает его и прикрепляет к главному через вызов метода primary.addSuppressed(closeException).
try (var r = new FaultyResource()) {
    r.doWork(); // Выбрасывает WorkException (Primary)
} // close() выбрасывает CloseException

// В результате стек-трейс сохраняет ОБА исключения:
// WorkException: Work failed
//     at ...
//     Suppressed: CloseException: Close failed
//         at ...

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

Вопросы для быстрой проверки

  1. Что произойдет, если исключение возникнет одновременно и в try, и в finally? Ответ: Исключение из блока finally полностью вытеснит исключение из блока try. Первичное исключение будет потеряно безвозвратно, а вызывающий код получит исключение, вылетевшее из finally.
  2. Что произойдет, если в блоке finally написать return 10;, когда в блоке try выброшено исключение? Ответ: Исключение будет полностью подавлено. Метод завершится штатно и вернет число 10. Подобная конструкция считается грубейшим антипаттерном.
  3. Почему при закрытии нескольких ресурсов в обычном блоке finally возникает утечка ресурсов? Ответ: Если закрытие первого ресурса завершится исключением, управление немедленно покинет блок finally. Инструкции закрытия последующих ресурсов не выполнятся, вызвав утечку нативных дескрипторов (файлов, сокетов).
  4. Как try-with-resources решает проблему вытеснения исключений блока finally? Ответ: Механизм TWR сохраняет ошибку из блока try в качестве основной (primary), а все ошибки, возникшие при закрытии ресурсов в методах close(), сохраняет внутри основного исключения с помощью метода addSuppressed(Throwable).

Типичные ошибки и Red Flags

  • ❌ Утверждение: “JVM объединит оба исключения в одно”: Без механизма try-with-resources обычный finally выбрасывает только последнее исключение, полностью затирая первое.
  • ❌ Использование операторов return, break или continue внутри finally: Ведет к немотивированному подавлению активных ошибок.
  • ❌ Попытка закрытия нескольких ресурсов в одном блоке finally одной строкой за другой без индивидуальных try-catch: Гарантированная утечка ресурсов при первом сбое.

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