W3docs

Иерархия исключений Java

Как Throwable, Error, Exception и RuntimeException связаны в иерархии классов исключений Java.

Каждое исключение в Java является частью небольшого дерева классов с корнем java.lang.Throwable. Знание формы этого дерева постоянно окупается: оно объясняет, почему catch (Exception e) не перехватывает OutOfMemoryError, почему RuntimeException особенный, и почему одни исключения требуют обработки, а другие — нет. Вся структура умещается в одну диаграмму.

На этой странице описано это дерево: корень Throwable, ветви Error и Exception, где проходит граница проверяемых/непроверяемых исключений, и как всё это определяет то, что именно ловят ваши блоки catch.

Всё дерево

Throwable
├── Error                       (unchecked — JVM-level)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   ├── VirtualMachineError
│   └── ...
└── Exception                   (checked by default)
    ├── IOException             (checked)
    ├── SQLException            (checked)
    ├── ClassNotFoundException  (checked)
    ├── ...
    └── RuntimeException        (unchecked)
        ├── NullPointerException
        ├── IllegalArgumentException
        ├── IndexOutOfBoundsException
        ├── ArithmeticException
        ├── ClassCastException
        ├── IllegalStateException
        └── ...

Всё дерево — это одна иерархия классов. Именно поэтому catch для суперtype перехватывает его подтипы, переменные исключений ведут себя как обычные ссылки, и вы можете хранить IOException в поле с типом Exception.

Throwable — корень

Throwable — это то, что принимает throw и объявляет catch. Всё, что вы хотите возбудить или обработать, является подклассом Throwable. Сам класс предоставляет:

  • Сообщение (getMessage())
  • Трассировку стека, захваченную при создании (getStackTrace(), printStackTrace())
  • Необязательную причину — другой Throwable, который вызвал данное исключение (getCause())
  • Подавленные исключения — вторичные сбои, присоединённые через try-with-resources (getSuppressed())

Вы почти никогда не расширяете Throwable напрямую. Интересная архитектура находится на уровень ниже.

Error — не перехватывайте

Error и его подклассы представляют сбои, о которых сигнализирует JVM: нехватка памяти, переполнение стека, файл класса, который не удаётся связать. По соглашению их не перехватывают в коде приложения, потому что:

  1. Обычно это означает, что JVM больше ненадёжна. Продолжение работы после OutOfMemoryError редко работает долго.
  2. Практически никогда нет осмысленного действия по восстановлению, которое мог бы предпринять ваш код.
  3. JVM сама может уже делать что-то с ними; перехват мешает этому.

Error технически можно поймать — Java вас не остановит. Но соглашение настолько сильное, что «перехватывает Error» считается красным флагом на код-ревью. Единственный законный случай использования — это супервизор верхнего уровня (обработчик запросов, исполнитель задач), который логирует и завершает работу чисто.

Exception — сбои приложения

Всё кроме Error под Throwable — это Exception или один из его подтипов. Граница между проверяемыми и непроверяемыми исключениями проходит внутри этой ветви, а не выше:

  • Прямые подклассы Exception, которые не являются RuntimeException, являются проверяемыми.
  • RuntimeException и все его подтипы являются непроверяемыми.

Именно поэтому catch (Exception e) соответствует и IOException (проверяемое), и NullPointerException (непроверяемое) — они являются братьями под одним корнем. Именно поэтому перехват Exception такой грубый: вы объединили обе ветви в одном блоке.

Различие между проверяемыми и непроверяемыми исключениями реально влияет на сигнатуры ваших методов — проверяемые исключения должны объявляться через throws или обрабатываться, непроверяемые — нет. Эта тема подробно рассмотрена в статье проверяемые vs. непроверяемые исключения.

RuntimeException — ветвь ошибок программирования

RuntimeException и его подтипы зарезервированы по соглашению для ошибок программирования, которых не должно быть в корректном коде:

  • NullPointerException — разыменование null
  • IllegalArgumentException — неверный аргумент
  • IllegalStateException — неверное состояние для операции
  • IndexOutOfBoundsException — индекс списка/массива за пределами
  • ArithmeticException — деление на ноль
  • ClassCastException — неверное приведение типов
  • UnsupportedOperationException — операция не поддерживается (например, изменение неизменяемого списка)

Вы можете бросать их из любого места без изменения сигнатуры метода. Вызывающие могут их перехватывать, но язык этого не требует. Это правильный инструмент, когда сбой говорит «это баг», а не «это иногда происходит».

Взаимосвязь типов в catch

catch (T e) соответствует любому выброшенному значению, которое является экземпляром T или подтипа T. Таким образом, иерархия напрямую определяет, что видят ваши блоки catch:

try { ... }
catch (IOException e)        { ... }   // catches FileNotFoundException too
catch (Exception e)          { ... }   // catches almost everything below Throwable
catch (Throwable t)          { ... }   // catches everything, including Error — don't

Порядок важен: поскольку каждый catch соответствует подтипам, вы должны перечислять наиболее конкретные типы первыми. Если catch (Exception e) стоял бы перед catch (IOException e), блок IOException был бы недостижимым и компилятор отклонил бы это. Смотрите несколько блоков catch для полных правил.

Именно поэтому конструкция «поймать всё» опасна. catch (Exception) перехватывает NullPointerException (баг) и IOException (восстановимый сбой) и IllegalStateException (вероятно, баг) — всё в одном блоке, без возможности обработать их по-разному. Иерархия требует быть более конкретным.

Поиск типов

Когда вы встречаете новое исключение в трассировке стека и хотите знать, где оно находится:

  • Оно в java.lang, если это фундаментальная ошибка (NullPointerException, ArithmeticException).
  • Оно в java.io, java.sql, java.net, если относится к домену этого пакета.
  • Класс, заканчивающийся на Error, почти наверняка находится под Error.
  • Класс, заканчивающийся на Exception, почти наверняка находится под Exception — но проверьте, расширяет ли он RuntimeException, чтобы узнать, является ли оно проверяемым.

Javadoc показывает цепочку наследования в верхней части каждой страницы. Если сомневаетесь, проверьте.

Рабочий пример

Небольшая программа, которая обходит иерархию с помощью проверок instanceof. Она перехватывает последовательность бросков как Throwable, затем сообщает, где каждый из них находится в дереве.

java— editable, runs on the server

Вспомогательный метод isChecked кодирует правило в одну строку: проверяемое подмножество — это Exception минус RuntimeException. Запустите программу, и вы увидите точно, где из пяти что находится: IOException является проверяемым, два RuntimeException — нет, OutOfMemoryError является Error (поэтому он не является ни Exception, ни проверяемым), а обычный Exception является проверяемым.

Что дальше

Встроенное дерево охватывает большинство случаев. Когда в вашей предметной области есть собственные сбои — «счёт не найден», «конфигурация устарела» — вы пишете собственные классы, расширяя нужный узел этого дерева. Перейдите к пользовательским исключениям Java.

Дополнительное чтение:

Практика

Практика
Программа выполняет `catch (Exception e)` вокруг блока. Что из этого будет поймано?
Программа выполняет `catch (Exception e)` вокруг блока. Что из этого будет поймано?
Was this page helpful?