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

Чи можна мати кілька catch блоків для одного try

Так, до одного блоку try можна прив'язати кілька блоків catch. Це дозволяє реалізувати диференційовану обробку різних типів помилок: наприклад, у разі мережевого збою виконати п...


🟢 Junior Level

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

Так, до одного блоку try можна прив’язати кілька блоків catch. Це дозволяє реалізувати диференційовану обробку різних типів помилок: наприклад, у разі мережевого збою виконати повторну спробу (retry), у разі помилки валідації повернути клієнту статус 400 Bad Request, а в разі неочікуваного збою бази даних залогувати критичну помилку.

Головне й обов’язкове правило: блоки catch повинні розташовуватися суворо від часткового до загального (від нащадків до суперкласів: наприклад, FileNotFoundException -> IOException -> Exception). Якщо розташувати батьківський клас раніше дочірнього, код не скомпілюється з помилкою exception ... has already been caught.


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

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

public class MultiCatchDemo {

    public String loadConfiguration(String filePath) {
        try {
            return Files.readString(Path.of(filePath));
        } catch (FileNotFoundException e) {
            // 1. Частковий випадок: файл відсутній -> застосовуємо дефолтну конфігурацію
            System.err.println("Config file missing, applying default fallback: " + e.getMessage());
            return "default.config=true";
        } catch (IOException e) {
            // 2. Більш загальний випадок I/O: помилка прав доступу, битий сектор диска
            System.err.println("I/O failure while accessing file: " + e.getMessage());
            throw new IllegalStateException("Cannot read configuration", e);
        } catch (Exception e) {
            // 3. Найзагальніший обробник: будь-які непередбачені збої
            System.err.println("Unexpected failure: " + e.getMessage());
            throw e;
        }
    }
}

🟡 Middle Level

Внутрішня будова: Таблиця винятків JVM (Exception Table)

На рівні байткоду конструкція try-catch компілюється в спеціальну структуру у .class-файлі — Exception Table (Таблиця винятків методу). Кожен рядок таблиці пов’язує діапазон байткоду з конкретним обробником:

from (байткод початку try) to (байткод кінця try) target (адреса входу в catch) catch_type (клас винятку)
0 8 11 java/io/FileNotFoundException
0 8 25 java/io/IOException
0 8 40 java/lang/Exception

Механізм “First Match Wins”:

  1. Коли віртуальна машина виконує інструкцію athrow, вона призупиняє виконання та сканує Exception Table методу згори донизу.
  2. JVM перевіряє кожен запис: чи є викинутий об’єкт екземпляром класу catch_type (або його підкласом).
  3. Перша умова, що збіглася, негайно захоплює керування. Решта записів таблиці ігноруються.
  4. Саме тому, якщо помістити catch (Exception e) першим записом, він перехопить абсолютно будь-який виняток, а наступні блоки стануть недосяжним кодом (Unreachable Code), що компілятор javac присікає ще на етапі компіляції.

Продуктивність: Вартість блоків try-catch (Zero-cost Exception Handling)

Поширений міф: «Що більше блоків catch, то повільніше працює код».

У сучасних JVM (HotSpot) реалізовано модель Zero-cost Exception Handling:

  • Наявність 1, 5 чи 10 блоків catch не сповільнює виконання коду всередині блоку try на жодну наносекунду, поки виняток не викинуто.
  • У байткод нормального потоку виконання не вставляються жодні додаткові if-else перевірки.
  • Накладні витрати (пошук у таблиці винятків і формування стек-трейсу) виникають виключно в момент реального виникнення збою (throw).

Вибір: Кілька блоків catch vs Multi-Catch (Java 7+)

Починаючи з Java 7, синтаксис multi-catch дозволяє об’єднувати кілька не пов’язаних успадкуванням типів в один блок:

// Погано: дублювання одного й того самого обробника
try {
    process();
} catch (SQLException e) {
    log.error("Storage error", e);
    throw new ServiceException(e);
} catch (IOException e) {
    log.error("Storage error", e);
    throw new ServiceException(e);
}

// Чудово: multi-catch для однакової логіки
try {
    process();
} catch (SQLException | IOException e) {
    log.error("Storage error", e);
    throw new ServiceException(e);
}

Правило вибору: Використовуйте кілька окремих блоків catch лише тоді, коли логіка обробки фундаментально різниться (наприклад, один тип винятку повертає дефолтне значення, а інший викидає доменну помилку). Якщо реакція ідентична — використовуйте multi-catch.


🔴 Senior Level

Ліміт розміру байткоду методів (64 KB Method Limit)

У специфікації віртуальної машини Java розмір байткоду одного методу обмежений значенням 65 535 байтів (64 KB). Якщо метод перевантажений десятками перевірок і довгим ланцюжком блоків catch, що містять вкладену логіку:

  1. Кожен запис у Exception Table збільшує розмір метаданих методу.
  2. Вкладені конструкції try-finally дублюють код очищення під кожен вихід.
  3. Перевищення 64 KB призводить до фатальної помилки компілятора: code too large for try statement.
  4. Великі методи (розміром понад 8000 байтів байткоду) дискваліфікуються з механізму JIT-інлайнінгу (Inlining Threshold), спричиняючи загальну деградацію продуктивності.

Архітектурне рішення: Дотримання принципу єдиної відповідальності (SRP). Метод повинен виконувати одну дію та делегувати обробку спеціалізованим обробникам або глобальним аспектам (@RestControllerAdvice).


Pattern Matching for switch (Java 21) як альтернатива

У Java 21 зіставлення зі зразком для оператора switch відкриває альтернативний спосіб диспетчеризації помилок, якщо виняток передається як об’єкт (наприклад, у реактивних стрімах або CompletableFuture.exceptionally()):

public void handleFailure(Throwable error) {
    // Диспетчеризація через сучасний pattern matching Java 21
    String response = switch (error) {
        case FileNotFoundException fnf -> "File missing: " + fnf.getMessage();
        case IOException io            -> "I/O error: " + io.getMessage();
        case TimeoutException to       -> "Request timeout: " + to.getMessage();
        case null, default             -> "General failure: " + error.getMessage();
    };
    System.err.println(response);
}

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

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

  1. Чи сповільнює код наявність великої кількості блоків catch, якщо винятки не виникають? Відповідь: Ні. HotSpot JVM використовує модель Zero-cost Exception Handling. Таблиця винятків (Exception Table) зберігається в метаданих класу, і пошук по ній запускається лише в момент викидання винятку через інструкцію athrow. У нормальному потоці виконання блоки catch не споживають тактів CPU.
  2. Чому компілятор видає помилку, якщо батьківський виняток (Exception) поставити раніше дочірнього (IOException)? Відповідь: Через принцип «First match wins» віртуальної машини. Перший же блок catch (Exception e) перехопить усі можливі помилки, зробивши блок catch (IOException e) мертвим недосяжним кодом (Unreachable Code), що заборонено специфікацією мови (JLS §14.21).
  3. Як JVM зіставляє викинутий виняток із кількома блоками catch? Відповідь: JVM послідовно згори донизу сканує записи Exception Table скомпільованого методу. Перший запис, діапазон байткоду якого покриває поточну інструкцію і тип якого є класом або суперкласом викинутого об’єкта, отримує керування.
  4. Коли виправдане використання кількох блоків catch, а коли краще об’єднати їх у multi-catch? Відповідь: Кілька блоків виправдані, коли реакція програми на різні типи помилок кардинально різниться (наприклад, повтор запиту під час мережевої помилки проти відкату транзакції під час помилки БД). Якщо реакція однакова (наприклад, просте логування та загортання), слід використовувати multi-catch (catch (A | B e)).

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

  • ❌ Спроба розташувати catch (Exception e) на початку списку обробників: Демонструє нерозуміння правил успадкування та компіляції.
  • ❌ Дублювання одного й того самого тіла в 5 блоках catch поспіль: Невміння використовувати синтаксис multi-catch (Java 7+).
  • ❌ Твердження: “Велика кількість catch сповільнює блок try”: Незнання концепції Zero-cost Exception Handling.
  • ❌ Розміщення catch (Throwable t) у локальному сервісному методі: Небезпечно, оскільки ненавмисно перехоплює фатальні помилки JVM (OutOfMemoryError, StackOverflowError).

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