Питання 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]]
  • [[Як правильно логувати винятки]]