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

Що таке multi-catch

Вона вирішує проблему дублювання однакового коду обробки для різних помилок.


🟢 Junior Level

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

Multi-Catch — це синтаксична конструкція Java (запроваджена в Java 7 у межах проєкту Project Coin / JSR 334), яка дозволяє перехоплювати кілька різнорідних типів винятків в одному блоці catch, перераховуючи їх через символ вертикальної риски |: catch (IOException | SQLException e).

Вона вирішує проблему дублювання однакового коду обробки для різних помилок.

Під час використання multi-catch діють два суворі правила компілятора:

  1. Змінна e є неявно фінальною (final): її значення не можна перепризначити (e = new ... викличе помилку компіляції).
  2. Типи винятків не повинні успадковуватися один від одного: запис catch (FileNotFoundException | IOException e) заборонений, оскільки дочірній тип уже включений у батьківський.

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

import java.io.IOException;
import java.sql.SQLException;

public class MultiCatchExample {

    public void processData() {
        try {
            readExternalFile();
            saveToDatabase();
        } catch (IOException | SQLException e) {
            // Єдина логіка для обох неперетинних збоїв
            // e.getClass() поверне реальний тип виниклого винятку
            System.err.println("Infrastructure failure: " + e.getMessage());
            throw new ApplicationServiceException("Data processing failed", e);
        }
    }

    private void readExternalFile() throws IOException { }
    private void saveToDatabase() throws SQLException { }

    static class ApplicationServiceException extends RuntimeException {
        public ApplicationServiceException(String msg, Throwable cause) { super(msg, cause); }
    }
}

🟡 Middle Level

Правило заборони успадкування: чому не можна SubClass | SuperClass?

Якщо в секції multi-catch вказати класи, що перебувають в одній ієрархії успадкування, компілятор javac видасть помилку:

// ❌ ПОМИЛКА КОМПІЛЯЦІЇ:
// Alternatives in a multi-catch statement cannot be related by subclassing
// Alternative 'java.io.FileNotFoundException' is a subclass of 'java.io.IOException'
try {
    readFile();
} catch (FileNotFoundException | IOException e) {
    // ...
}

Причина заборони:

Конструкція FileNotFoundException | IOException є надлишковою (Redundant). Оскільки FileNotFoundException вже є IOException, тип IOException гарантовано перехоплює всі підтипи. Специфікація Java (JLS §14.20) навмисно забороняє таке об’єднання, щоб запобігти появі логічних помилок та візуального сміття в кодовій базі.

Як виправити:

  • Або залишити лише батьківський клас: catch (IOException e) (якщо обробка однакова).
  • Або рознести по окремих блоках catch, поставивши частковий першим (якщо обробка різниться).

Чому змінна e в Multi-Catch суворо final?

У звичайному блоці catch (IOException e) змінна e формально не є фінальною, і розробник може переприсвоїти посилання: e = new IOException().

У блоці multi-catch змінна e завжди неявно оголошена як final:

try {
    run();
} catch (IOException | SQLException e) {
    // ❌ ПОМИЛКА КОМПІЛЯЦІЇ:
    // Multi-catch parameter e is final and cannot be assigned
    // e = new IOException(); 
}

Архітектурне обґрунтування: Статичний тип змінної e у multi-catch є синтетичним типом перетину — найменшим спільним супертипом (Least Upper Bound). Якби дозволялося присвоєння e = new IOException(), виникла б діра в статичній типізації: змінна e перестала б відповідати типу SQLException, якщо первинний виняток був саме ним.


Як Multi-Catch влаштований у байткоді JVM

Попри об’єднаний синтаксис вихідного Java-коду, на рівні байткоду віртуальної машини Java немає спеціальної інструкції multi-catch.

Компілятор генерує в Exception Table методу окремі незалежні рядки для кожного типу зі списку |, але вказує для них одну й ту саму адресу інструкції переходу (target):

Exception table:
   from    to  target type
       0     8    12   Class java/io/IOException
       0     8    12   Class java/sql/SQLException

Перевага: Вихідний код чистий і не містить дублювання, а в байткоді генерується рівно один екземпляр тіла обробника, зменшуючи розмір скомпільованого .class-файлу.


🔴 Senior Level

Precise Rethrow (Точний прокид винятків у Java 7+)

Одночасно з multi-catch у Java 7 була представлена потужна оптимізація аналізу потоку типів компілятора — Precise Rethrow:

public class PreciseRethrowDemo {

    // У сигнатурі методу оголошено ТОЧНІ типи, а не throws Exception!
    public void process() throws IOException, SQLException {
        try {
            if (System.currentTimeMillis() % 2 == 0) {
                throw new IOException("File error");
            } else {
                throw new SQLException("DB error");
            }
        } catch (Exception e) { // Перехоплюємо широкий тип Exception!
            System.err.println("Logging error before rethrow: " + e.getMessage());
            // Компілятор javac бачить, що e не переприсвоювалася (effectively final),
            // аналізує блок try і точно знає, що вилетіти можуть ТІЛЬКИ IOException та SQLException!
            throw e; 
        }
    }
}

До Java 7 при перехопленні catch (Exception e) повторний прокид throw e вимагав обов’язкового оголошення throws Exception у заголовку методу. Завдяки механізму Precise Rethrow компілятор зберігає точну статичну типізацію.


Канонічний сценарій: Reflection API

До появи Multi-Catch робота зі стандартним Reflection API перетворювалася на гігантські «драбинки» однакових обробників. З multi-catch код стає лаконічним:

try {
    Class<?> clazz = Class.forName("com.service.Worker");
    Method method = clazz.getMethod("execute");
    method.invoke(clazz.getDeclaredConstructor().newInstance());
} catch (ClassNotFoundException | NoSuchMethodException | 
         InstantiationException | IllegalAccessException | 
         InvocationTargetException e) {
    log.error("Reflection initialization failed for reason: {}", e.getClass().getSimpleName(), e);
    throw new FrameworkInitException(e);
}

Пастка переривання потоку (InterruptedException у Multi-Catch)

Груба помилка — об’єднання InterruptedException зі звичайними винятками введення-виведення без відновлення прапорця:

// ❌ ПРИХОВАНИЙ БАГ: втрата статусу переривання!
try {
    Thread.sleep(100);
    socket.read();
} catch (InterruptedException | IOException e) {
    log.error("Error", e);
    // Якщо впав InterruptedException, потік очистив прапорець переривання!
    // Зовнішній пул потоків не дізнається про необхідність зупинки!
}

// ✅ ПРАВИЛЬНО:
try {
    Thread.sleep(100);
    socket.read();
} catch (InterruptedException | IOException e) {
    if (e instanceof InterruptedException) {
        Thread.currentThread().interrupt(); // Відновлюємо статус!
    }
    log.error("Error", e);
}

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

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

  1. Чому в multi-catch не можна вказати батьківський і дочірній класи (наприклад, FileNotFoundException | IOException)? Відповідь: Дочірній клас є підтипом батьківського, тому батьківський клас уже перехоплює всі його екземпляри. Таке оголошення надлишкове і заборонене специфікацією мови (JLS §14.20).
  2. Чому змінна винятку e в multi-catch неявно оголошена як final? Відповідь: Тому що статичним типом змінної e є об’єднання типів (Least Upper Bound). Якби дозволялося присвоєння e = new AnotherException(), статична типобезпека інших альтернатив була б порушена.
  3. Як віртуальна машина (JVM) виконує блок multi-catch на рівні байткоду? Відповідь: У таблиці винятків методу (Exception Table) компілятор створює окремі незалежні записи для кожного класу, зазначеного в multi-catch, але спрямовує їх на єдину адресу байткоду тіла обробника.
  4. Що таке Precise Rethrow у Java 7+? Відповідь: Це здатність компілятора javac точно визначати типи винятків під час повторного throw e з блоку catch (Exception e). Якщо змінна e не переприсвоювалася, компілятор аналізує блок try і вимагає вказати в throws лише реально можливі типи, а не загальний Exception.

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

  • ❌ Спроба перепризначити змінну e в блоці multi-catch: Порушує вимогу неявної фінальності (final).
  • ❌ Об’єднання типів, пов’язаних відношенням успадкування: Не компілюється.
  • ❌ Об’єднання InterruptedException у multi-catch без відновлення прапорця: Призводить до ігнорування команди скасування потоку.
  • ❌ Твердження: “Multi-Catch у байткоді генерує спільний клас-обгортку”: У байткоді створюються звичайні роздільні записи в Exception Table.

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