Avatar ·

Проверяемые и непроверяемые исключения в Java: в чем разница и как их использовать?

Всем привет. Я начал изучать Java и никак не могу понять разницу между checked (проверяемыми) и unchecked (непроверяемыми) исключениями. Читал документацию, но на практике всё равно путаюсь. Например, я пишу простой метод, который читает файл: ```java public void readFile(String path) { FileReader file = new FileReader(path); BufferedReader reader = new BufferedReader(file); String line = reader.readLine(); System.out.println(line); } ``` Компилятор ругается на `FileReader` и `readLine()` — пишет, что нужно обработать `FileNotFoundException` и `IOException`. Я оборачиваю в `try-catch` и всё работает. Но потом я пишу метод, который делит числа: ```java public int divide(int a, int b) { return a / b; } ``` Если передать `b = 0`, программа падает с `ArithmeticException`, но компилятор не требует ничего обрабатывать. Почему так? В чем логика? Когда нужно использовать `throws`, а когда `try-catch`? И как правильно проектировать свои исключения — наследовать от `Exception` или от `RuntimeException`? Буду благодарен за понятное объяснение с примерами. <<>>

Ключевое отличие: контроль компилятора

Главное различие между checked и unchecked исключениями в Java — это то, как с ними работает компилятор. Checked исключения (наследники Exception, но не RuntimeException) компилятор заставляет вас обрабатывать — либо через try-catch, либо объявляя throws. Unchecked исключения (наследники RuntimeException и Error) компилятор не проверяет — вы можете их игнорировать, хотя программа может упасть во время выполнения.

Философия дизайна

Checked исключения созданы для восстановимых ошибок, которые возникают не по вине программы, а из-за внешних факторов: файл не найден, сеть недоступна, база данных отключена. Разработчик может и должен что-то сделать — показать сообщение пользователю, повторить операцию, использовать резервный сервер.

Unchecked исключения — это ошибки программирования: деление на ноль, выход за границы массива, вызов метода на null. Их причина — баг в коде. Обрабатывать их на каждый чих бессмысленно, нужно исправлять код. Или использовать их для ошибок, которые невозможно предвидеть (например, неверные аргументы, переданные извне).

Практические правила

  • Checked (проверяемые): IOException, SQLException, FileNotFoundException. Используйте, когда вызывающий код может разумно восстановиться.
  • Unchecked (непроверяемые): NullPointerException, ArithmeticException, IllegalArgumentException. Используйте для ошибок, которые указывают на баг или нарушение контракта.

Примеры кода

Checked исключение — обрабатываем обязательно

public void readFile(String path) {
    try {
        FileReader file = new FileReader(path);
        BufferedReader reader = new BufferedReader(file);
        String line = reader.readLine();
        System.out.println(line);
    } catch (FileNotFoundException e) {
        System.err.println("Файл не найден: " + path);
        // Можно создать файл по умолчанию или запросить другой путь
    } catch (IOException e) {
        System.err.println("Ошибка чтения файла: " + e.getMessage());
    }
}

Или можно "прокинуть" исключение выше:

public void readFile(String path) throws IOException {
    FileReader file = new FileReader(path);
    BufferedReader reader = new BufferedReader(file);
    String line = reader.readLine();
    System.out.println(line);
}

Unchecked исключение — компилятор молчит, но код может упасть

public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("Деление на ноль недопустимо");
    }
    return a / b;
}

Компилятор не заставляет обрабатывать IllegalArgumentException — это ваша ответственность проверять аргументы перед вызовом.

Как создавать свои исключения

  • Наследуйте от Exception, если ошибка может быть исправлена вызывающим кодом. Например, AccountNotFoundException — можно попросить ввести другой номер счёта.
  • Наследуйте от RuntimeException, если ошибка — следствие бага или нарушения контракта. Например, InvalidEmailFormatException — вызывающий код должен был проверить формат до вызова.

Золотое правило (из Effective Java, Джошуа Блох)

Используйте checked исключения для восстановимых условий и unchecked — для ошибок программирования. Не злоупотребляйте checked исключениями — если вызывающий код не может ничего сделать с ошибкой, сделайте её unchecked. Иначе вы заставите каждого клиента вашего метода писать пустые try-catch, что ухудшает читаемость.

<<>>

Дополнение: когда checked исключения — это зло

На практике многие Java-разработчики (включая авторов популярных фреймворков) считают, что checked исключения в современной разработке приносят больше проблем, чем пользы. Например, в Java 8+ с лямбдами и Stream API checked исключения очень неудобны — вы не можете просто выбросить IOException внутри лямбды, не оборачивая его в unchecked.

Поэтому часто используют такой подход: checked исключения перехватывают на нижнем уровне и оборачивают в unchecked (например, в RuntimeException), а обрабатывают уже в верхнем слое (контроллер, UI). Это упрощает код и делает его более гибким.

Пример:

public String readFileContent(String path) {
    try {
        return Files.readString(Path.of(path));
    } catch (IOException e) {
        throw new RuntimeException("Ошибка чтения файла: " + path, e);
    }
}

Такой подход называется "unchecked wrapping" и используется в Spring, Hibernate и многих других библиотеках. Главное — не терять оригинальное исключение (передавайте его в конструктор как cause).

В итоге: если вы пишете библиотеку для широкого круга пользователей — используйте checked исключения для важных восстановимых ошибок. Если пишете приложение — чаще используйте unchecked, чтобы не загромождать код.