Как устроена иерархия исключений в Java и почему она имеет такую структуру?

«Как устроена иерархия исключений в Java и почему она имеет такую структуру?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Иерархия исключений Java построена вокруг класса Throwable и разделена на две основные ветви: Error и Exception. Эта структура позволяет четко классифицировать проблемы по их природе и обязательности обработки.

graph TD
    A[Throwable] --> B[Error]
    A --> C[Exception]
    C --> D[RuntimeException]
    C --> E[Проверяемые исключения<br>e.g., IOException, SQLException]

    B --> F[VirtualMachineError<br>e.g., OutOfMemoryError]
    B --> G[LinkageError<br>e.g., NoClassDefFoundError]

    D --> H[NullPointerException]
    D --> I[IllegalArgumentException]
    D --> J[IndexOutOfBoundsException]

Логика разделения:

  1. Error и его подклассы:

    • Что это: Критические, непроверяемые ошибки виртуальной машины или системы (например, OutOfMemoryError, StackOverflowError).
    • Почему отдельно: Они указывают на проблемы, которые приложение обычно не может и не должно обрабатывать. Восстановление часто невозможно.
  2. Exception и его подклассы:

    • RuntimeException и его подклассы:
      • Что это: Непроверяемые исключения, указывающие на ошибки программиста или логики (например, NullPointerException, IllegalArgumentException).
      • Почему непроверяемые: Обработка таких исключений в каждом месте была бы слишком обременительной. Их можно предотвратить корректным кодом.
    • Другие подклассы Exception (проверяемые исключения):
      • Что это: Исключительные ситуации, которые программа может и должна обработать (например, IOException, SQLException).
      • Почему проверяемые: Компилятор требует их явной обработки (через try-catch) или объявления в сигнатуре метода (throws). Это гарантирует, что разработчик задумается о восстановлении после ожидаемых внешних проблем.

Пример, иллюстрирующий разницу:

// Проверяемое исключение - компилятор заставит обработать
try {
    Files.readAllLines(Paths.get("file.txt"));
} catch (IOException e) { // ОБЯЗАТЕЛЬНАЯ обработка
    System.err.println("Файл не найден.");
}

// Непроверяемое исключение (Runtime) - обработка опциональна
String str = null;
// Следующая строка выбросит NullPointerException, но компилятор не требует try-catch.
// System.out.println(str.length());

Итог: Иерархия разделяет ответственность: Error — для JVM, непроверяемые RuntimeException — для программиста, проверяемые Exception — для контролируемого восстановления от внешних ошибок.