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

В якому порядку потрібно розташовувати catch блоки

Блоки catch повинні розташовуватися суворо від часткового до загального (від класів-нащадків до класів-предків за ієрархією успадкування).


🟢 Junior Level

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

Блоки catch повинні розташовуватися суворо від часткового до загального (від класів-нащадків до класів-предків за ієрархією успадкування).

Якщо поставити суперклас раніше підкласу (наприклад, спочатку catch (IOException e), а потім catch (FileNotFoundException e)), код не скомпілюється: компілятор javac видасть помилку exception ... has already been caught, оскільки нижній блок стане недосяжним (Unreachable Code).

Якщо винятки не пов’язані успадкуванням (є сиблінгами, наприклад SQLException та IOException), їх можна розташовувати в будь-якому довільному порядку.


Базовий приклад на Java 21

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CatchOrderingDemo {

    public void processFile(String path) {
        try {
            Files.readString(Path.of(path));
        } catch (FileNotFoundException e) {
            // 1. Найбільш конкретний (нащадок IOException)
            System.err.println("File not found on disk: " + e.getMessage());
        } catch (IOException e) {
            // 2. Більш загальний суперклас (батько FileNotFoundException)
            System.err.println("General I/O failure: " + e.getMessage());
        } catch (Exception e) {
            // 3. Найзагальніший базовий тип застосунку (предок усіх перевірюваних винятків)
            System.err.println("Unexpected failure: " + e.getMessage());
        }
    }
}

Що станеться при порушенні порядку?

public void brokenOrder() {
    try {
        // ...
    } catch (IOException e) {
        // Цей блок перехопить абсолютно ВСІ помилки введення-виведення, включно з FileNotFoundException
    } 
    // ❌ ПОМИЛКА КОМПІЛЯЦІЇ:
    // exception java.io.FileNotFoundException has already been caught
    // catch (FileNotFoundException e) { 
    //     // Сюди керування не зможе потрапити НІКОЛИ!
    // }
}

🟡 Middle Level

Механізм JVM: First Match Wins в Exception Table

У скомпільованому байткоді методи містять атрибут Exception Table (таблиця винятків). Порядок записів у цій таблиці в точності повторює порядок слідування блоків catch у вихідному коді:

Exception table:
   from    to  target type
       0     6     9   Class java/io/FileNotFoundException
       0     6    18   Class java/io/IOException
       0     6    27   Class java/lang/Exception

Алгоритм обробки в рантаймі:

  1. У разі виникнення винятку віртуальна машина HotSpot починає лінійне сканування таблиці згори донизу.
  2. Для кожного елемента таблиці JVM перевіряє: чи покриває діапазон [from, to) поточну інструкцію байткоду і чи є викинутий об’єкт сумісним із типом type.
  3. Перший запис, що збігся, захоплює керування, і потік виконання переходить на адресу target.
  4. Усі наступні записи таблиці ігноруються, навіть якщо серед них є більш точні типи.

Саме тому компілятор javac на етапі статичного аналізу блокує розміщення суперкласів вище за підкласи — специфікація Java (JLS §14.21) суворо забороняє створення недосяжного байткоду.


Коли порядок блоків catch НЕ має значення?

  1. Непов’язані винятки (Сиблінги):
    // Порядок між IOException та SQLException абсолютно байдужий:
    try {
        // ...
    } catch (IOException e) {
        // ...
    } catch (SQLException e) {
        // ...
    }
    

    Жоден із них не є нащадком іншого, тому вони ніколи не перехоплять помилки один одного.

  2. Усередині одного блоку multi-catch:
    // Варіант А і Варіант Б абсолютно ідентичні:
    catch (IOException | SQLException e) { }
    catch (SQLException | IOException e) { }
    

    У multi-catch усі типи розділяють єдину адресу target у таблиці винятків.


Взаємодія з Try-With-Resources

Якщо в конструкції try-with-resources метод close() викидає виняток, він перехоплюється блоками catch поточного оператора try:

  • Якщо тіло try виконалося успішно, а close() упав, виняток із close() спрямовується до відповідного блоку catch за загальними правилами ієрархії.
  • Якщо впали і тіло try, і close(), у catch потрапить первинний виняток із try, а виняток закриття ресурсу міститиметься всередині нього у списку пригнічених (getSuppressed()).

🔴 Senior Level

Оптимізація розташування в Highload: Hot Exceptions First

Хоча в переважній більшості випадків пошук в Exception Table виконується швидко, лінійне сканування має складність $O(N)$, де $N$ — число записів у таблиці.

У високонавантажених методах, що обробляють винятки в щільному потоці:

  • Серед не пов’язаних між собою типів (сиблінгів) найчастіше виникаючі винятки слід розміщувати вище.
  • Це мінімізує кількість ітерацій перевірки типів у нативному коді JVM перед передачею керування в обробник.

Перехоплення Throwable та Error: небезпеки та межі застосування

Розміщення catch (Throwable t) наприкінці списку блоків перехоплення — потужна, але небезпечна практика.

try {
    executeTask();
} catch (BusinessException e) {
    // 1. Очікувані доменні помилки
} catch (Throwable t) {
    // 2. Ловить ВСЕ, включно з Error (OutOfMemoryError, StackOverflowError, LinkageError)
    log.error("Fatal system crash", t);
}

Архітектурне правило розміщення catch (Throwable):

  • Категорично заборонено: використовувати catch (Throwable) у середині бізнес-логіки (всередині сервісів, репозиторіїв, допоміжних утиліт).
  • Дозволено та необхідно: використовувати catch (Throwable) виключно на зовнішніх межах потоків виконання:
    1. У корені робочого потоку пулу (Thread.run() / ThreadPoolExecutor.afterExecute()).
    2. В обробниках подій мережевих фреймворків (Netty Channel Pipeline).
    3. У глобальних диспетчерах веб-запитів (Spring DispatcherServlet, @RestControllerAdvice).

Чому: Якщо бізнес-код перехопить OutOfMemoryError або VirtualMachineError і продовжить нормальну роботу, JVM опиниться в неконсистентному стані з пошкодженою пам’яттю, зламаними блокуваннями моніторів та ризиком непередбачуваної поведінки.


🎯 Шпаргалка для інтерв’ю

Питання для швидкої перевірки

  1. В якому порядку необхідно розташовувати блоки catch? Відповідь: Суворо від часткового до загального (від класів-нащадків до суперкласів: наприклад, FileNotFoundException -> IOException -> Exception). Не пов’язані між собою класи можна розташовувати в будь-якому порядку.
  2. Що станеться, якщо розташувати catch (Exception e) перед catch (IOException e)? Відповідь: Код не скомпілюється. Компілятор видасть помилку exception IOException has already been caught, оскільки блок catch (IOException) стане недосяжним.
  3. Чи має значення порядок перерахування класів усередині multi-catch (catch (A | B e))? Відповідь: Ні, порядок усередині multi-catch не має значення. В Exception Table компілятор створює окремі записи, що вказують на одну й ту саму адресу байткоду обробника.
  4. Де припустимо використовувати блок catch (Throwable t)? Відповідь: Тільки на найзовнішніших межах виконання застосунку (головний цикл потоку, глобальний фільтр веб-сервера) для аварійного логування та перезапуску завдання. У прикладному бізнес-коді ловити Throwable не можна, оскільки це перехоплює фатальні системні помилки JVM (OutOfMemoryError).

Типові помилки та Red Flags

  • ❌ Спроба поставити батьківський виняток перед дочірнім: Базова помилка компіляції.
  • ❌ Твердження: “Порядок непов’язаних винятків важливий для компілятора”: Якщо класи не перебувають у спорідненості, порядок довільний.
  • ❌ Використання catch (Throwable) всередині транзакційних методів: Маскує системні падіння JVM і перешкоджає нормальному аварійному завершенню процесу.
  • ❌ Спроба ловити дочірній клас усередині multi-catch поруч із його батьком: catch (FileNotFoundException | IOException e) викличе помилку компіляції.

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