Вопрос 27 · Раздел 7

В каком порядке располагать catch блоки?

Блоки catch должны идти от более конкретных к более общим:

Версии по языкам: English Russian Ukrainian

Junior Level

Главное правило: от частного к общему

Блоки catch должны идти от более конкретных к более общим:

try {
    Files.readAllLines(Paths.get("file.txt"));
} catch (FileNotFoundException e) {
    // 1. Сначала частный случай
    System.out.println("File not found");
} catch (IOException e) {
    // 2. Затем более общий
    System.out.println("IO error");
} catch (Exception e) {
    // 3. Самый общий — в конце
    System.out.println("Unknown error");
}

Почему так

Если поставить общий catch первым, он перехватит всё, и частные catch никогда не выполнятся:

// Ошибка компиляции!
try {
    // код
} catch (Exception e) {
    // Перехватит всё, включая IOException
} catch (IOException e) {
    // Никогда не выполнится — код недостижим
}

Компилятор выдаст: "exception IOException has already been caught".

Механизм “first match wins” на уровне компилятора

На этапе компиляции:

Компилятор javac проверяет каждый catch блок последовательно сверху вниз (JLS 14.20). Для каждого нового catch он задаёт вопрос: “Является ли тип этого catch подтипом любого из catch, которые уже были выше?”

  • Если да — компилятор выдаёт ошибку, потому что код в этом catch блоке недостижим (unreachable code, JLS 14.21).
  • Если нет — catch добавляется в Exception Table класса.

Это статическая проверка — компилятор не допускает компиляции с неправильным порядком. Поэтому в правильно скомпилированном коде порядок всегда корректен.

На этапе выполнения (JVM):

Exception Table — это массив записей {from, to, target, type}. При выбросе исключения JVM:

  1. Ищет первую запись, где тип совпадает с исключением (или его супертипом).
  2. Переходит на target — адрес соответствующего catch блока.
  3. Остальные записи игнорируются.

Поэтому “first match” — это буквально первый совпавший entry в Exception Table.

Простое правило

Располагайте по иерархии наследования — от потомков к предкам.

Когда НЕ следовать правилу “от частного к общему”

  1. Никогда не нарушайте — это правило enforced компилятором, обратный порядок не скомпилируется
  2. Multi-catch внутри — порядок типов в catch (A | B e) не имеет значения, потому что оба типа указывают на один target в Exception Table
  3. Не ловите Throwable в бизнес-логике — только на границе системы
  4. Не ставьте редкие исключения перед частыми — если FileNotFoundException случается в 100x чаще EOFException, ловите его первым для микро-оптимизации поиска в Exception Table
  5. Не смешивайте уровни абстракции — не чередуйте бизнес- и инфраструктурные исключения без логики

Caveat: Java 7+ multi-catch изменяет подход к ordering

С появлением multi-catch в Java 7, вопрос порядка становится менее критичным в некоторых сценариях. Если у вас есть 3+ исключения с одинаковой обработкой, вместо:

// 3 catch блока — порядок важен (FileNotFoundException перед IOException)
catch (FileNotFoundException e) { handle(); }
catch (SQLException e) { handle(); }
catch (IOException e) { handle(); }

Можно написать:

// 1 catch блок — порядок внутри multi-catch НЕ важен
catch (FileNotFoundException | SQLException | IOException e) { handle(); }

Multi-catch снимает проблему ordering, потому что все типы обрабатываются одинаково и указывают на один target в Exception Table.

Когда порядок catch блоков НЕ важен

  1. Все catch блоки обрабатывают одинаково — если каждый catch делает log.error(e); throw new ServiceException(e);, порядок не влияет на результат. Однако в этом случае лучше использовать multi-catch.

  2. Исключения не связаны наследованиемcatch (SQLException e) и catch (NullPointerException e) не имеют общего подтипа. Они никогда не пересекутся, и порядок между ними не влияет на корректность (хотя может влиять на производительность поиска).

  3. Multi-catch — внутри catch (A | B e) порядок типов не имеет значения. Компилятор генерирует отдельные записи в Exception Table, но все они указывают на один target.

  4. Единственный catch блокcatch (Exception e) — нечего упорядочивать.


Middle Level

First Match Wins

JVM проходит Exception Table сверху вниз. Первое совпадение захватывает управление.

Стратегия расположения

1. Ожидаемые vs Исключительные: Самые частые бизнес-исключения (например, ValidationException) ловите первыми — они будут в начале Exception Table.

2. Разная обработка:

@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestBody OrderRequest req) {
    try {
        return ResponseEntity.ok(orderService.create(req));
    } catch (ValidationException e) {
        // 400 Bad Request — ошибка клиента
        return ResponseEntity.badRequest().body(e.getMessage());
    } catch (ResourceNotFoundException e) {
        // 404 Not Found
        return ResponseEntity.notFound().build();
    } catch (DataAccessException e) {
        // 500 Server Error — наша проблема
        log.error("Database error processing order", e);
        return ResponseEntity.internalServerError().build();
    }
}

Multi-catch порядок

В catch (A | B e) порядок внутри блока не имеет значения:

catch (IOException | SQLException e) { }
catch (SQLException | IOException e) { }
// Одно и то же

Но всё равно нельзя объединять родителя и наследника.

Взаимодействие с try-with-resources

Исключения при автоматическом закрытии ресурсов (close()) возникают до попадания в ваши catch блоки. Ваш catch может поймать исключение из close(), если в основном try ошибок не было.


Senior Level

Exception Table и производительность

Поиск в Exception Table — сложность O(n). В методах с десятками catch блоков это может иметь микроскопическое влияние на производительность. Размещайте самые частые исключения первыми.

Обработка Error

Если ловите Throwable, вы перехватываете и Error (напр. OutOfMemoryError):

// Правило: специфичные Exception выше Throwable
try { ... }
catch (BusinessException e) { }      // Бизнес-ошибки
catch (DataAccessException e) { }    // Инфраструктура
catch (Throwable t) { }              // Только в конце, на границе системы

Никогда не делайте catch (Throwable t) в середине бизнес-логики — только на самых внешних границах системы.

Exception Hiding

Неправильный порядок может “спрятать” важную ошибку в общем логе. Мониторинг не увидит разницы между ValidationException и OutOfMemoryError.

Static Analysis

Используйте Checkstyle или Sonar для гарантии единообразного подхода к иерархии перехвата во всём проекте.

Диагностика

  • javap -c — покажет Exception Table с приоритетами прыжков на метки catch
  • Метрики — считайте каждый тип исключения отдельно
  • Log Correlation — сохраняйте Trace ID для всех типов ошибок
  • Global Error Handler@ControllerAdvice ловит всё, что не поймали локально

🎯 Шпаргалка для интервью

Обязательно знать:

  • Главное правило: от частного к общему — потомки перед предками в Exception Table
  • Компилятор не допустит обратный порядок — ошибка “exception X has already been caught”
  • JVM проходит Exception Table сверху вниз, первое совпадение захватывает управление (first match wins)
  • Порядок внутри catch (A | B e) НЕ важен — все типы указывают на один target
  • Исключения не связанные наследованием можно располагать в любом порядке между собой
  • Размещайте самые частые исключения первыми для микро-оптимизации поиска
  • catch (Throwable t) — только на границе системы, никогда в бизнес-логике

Частые уточняющие вопросы:

  • Что будет если перепутать порядок? — Ошибка компиляции, код не скомпилируется
  • Влияет ли порядок на производительность? — O(n) поиск, частые исключения первыми
  • Когда порядок НЕ важен? — Multi-catch, несвязанные наследованием типы, единственный catch
  • Почему Throwable ловим только в конце? — Перехватит Error включая OutOfMemoryError

Красные флаги (НЕ говорить):

  • “Ставлю общий catch первым для удобства” — не скомпилируется
  • “Порядок в multi-catch важен” — нет, все типы → один target
  • “Ловлю Throwable в середине бизнес-логики” — перехватит Error, состояние непредсказуемо
  • “Порядок catch влияет на корректность скомпилированного кода” — компилятор гарантирует корректный порядок

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

  • [[24. Можно ли несколько catch блоков для одного try]] — несколько catch и first match wins
  • [[25. Что такое multi-catch (catching multiple exceptions)]] — multi-catch снимает проблему ordering
  • [[1. В чём разница между checked и unchecked exceptions]] — иерархия исключений
  • [[5. Что находится в вершине иерархии исключений]] — Throwable и Error
  • [[19. Почему не стоит глотать исключения (catch empty)]] — неправильная обработка