Как устроена система исключений (Exceptions) в Java?

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

Ответ

Исключения в Java — это механизм обработки ошибок и нестандартных условий выполнения программы, который отделяет нормальный поток управления от кода обработки ошибок.

Иерархия классов исключений:

Throwable (базовый класс)
├── Error (критические ошибки, которые обычно не обрабатываются)
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception (исключения, которые можно и нужно обрабатывать)
    ├── RuntimeException (Unchecked Exceptions)
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   └── IndexOutOfBoundsException
    └── Все остальные (Checked Exceptions)
        ├── IOException
        └── SQLException
Ключевые отличия Checked vs Unchecked: Аспект Checked Exceptions Unchecked Exceptions (RuntimeException)
Проверка компилятором Да. Метод должен либо обработать (catch), либо объявить в throws. Нет. Обработка не обязательна.
Происхождение Внешние по отношению к программе условия (файл не найден, сетевой сбой). Ошибки программиста (обращение по null, неверный аргумент).
Best Practice Восстановление или корректное завершение операции. Исправление кода. Часто не обрабатываются на низком уровне.

Блоки для обработки: try, catch, finally, try-with-resources.

Пример обработки с best practices:

// 1. Обработка с конкретными исключениями и логированием
public void readFile(String path) {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) { // try-with-resources
        String line = br.readLine();
        // ... обработка данных
    } catch (FileNotFoundException e) {
        // Конкретное исключение: файл не найден
        log.error("Файл не найден: " + path, e);
        throw new UserFriendlyMessageException("Пожалуйста, проверьте путь к файлу.");
    } catch (IOException e) {
        // Более общее исключение ввода-вывода
        log.error("Ошибка чтения файла: " + path, e);
        throw new RuntimeException("Внутренняя ошибка системы", e); // Обертывание (wrapping)
    }
    // finally не нужен, так как try-with-resources закроет ресурс автоматически
}

// 2. Создание собственных исключений (наследование от RuntimeException для unchecked)
public class InsufficientFundsException extends RuntimeException {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

// 3. Использование в коде
public void withdraw(double amount) {
    if (amount > balance) {
        throw new InsufficientFundsException("Недостаточно средств. Баланс: " + balance);
    }
    // ... снятие средств
}

Важные принципы:

  1. Не игнорируйте исключения: Пустой блок catch скрывает ошибки.
  2. Предоставляйте информативные сообщения: Это упрощает отладку.
  3. Используйте checked exceptions для восстановимых условий, а unchecked — для ошибок программиста.
  4. Освобождайте ресурсы: Используйте try-with-resources или блок finally.