⚡ Розділ 7 · Питання #8

Чи завжди виконується блок finally

У стандартних умовах виконання програми блок finally виконується завжди — незалежно від того, чи завершився блок try успішно, чи було викинуто виняток, чи спрацював оператор ret...


🟢 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 через Linux 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 буде безповоротно втрачений (Shadowed / Masked). Викликаючий код отримає лише 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» (у разі аварії під час виклику методу lock(), unlock() у finally впаде з IllegalMonitorStateException).
  • ❌ «У try-with-resources блок finally виконується до закриття ресурсів» (навпаки, автоматичний close() викликається суворо до явних catch та finally).

Пов’язані теми