⚡ Раздел 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) вызовет ошибку компиляции.

Связанные темы