В чем разница между checked (проверяемыми) и unchecked (непроверяемыми) исключениями в Java?

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

Ответ

Различие основано на обязательности обработки на этапе компиляции.

Критерий Checked Exceptions (Проверяемые) Unchecked Exceptions (Непроверяемые / Runtime)
Наследование Классы, наследующие от java.lang.Exception, но не от RuntimeException. Классы, наследующие от java.lang.RuntimeException (а она — от Exception).
Проверка компилятором Да. Компилятор требует либо обработать исключение в try-catch, либо объявить в throws сигнатуре метода. Нет. Компилятор не требует их явного объявления или обработки.
Когда возникают Внешние, часто восстанавливаемые условия, на которые программа может повлиять (отсутствие файла, разрыв сети, ошибка БД). Внутренние ошибки программиста или логики приложения (обращение к null, неверный индекс массива, ошибка бизнес-логики).
Примеры IOException, SQLException, ClassNotFoundException, FileNotFoundException. NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ArithmeticException.
Best Practice Использовать для ошибок, которые разумно ожидать и от которых вызывающий код может восстановиться. Использовать для ошибок, которые указывают на баг в программе и от которых обычно нельзя корректно восстановиться.

Пример кода:

// CHECKED Исключение: Компилятор заставит его обработать.
public String readConfigFile(String path) throws IOException { // Объявляем в throws
    return Files.readString(Path.of(path)); // Может выбросить FileNotFoundException (checked)
}
// Или обрабатываем:
public String readConfigFileSafe(String path) {
    try {
        return Files.readString(Path.of(path));
    } catch (IOException e) { // Ловим checked исключение
        return "default"; // Восстанавливаемся
    }
}

// UNCHECKED Исключение: Компилятор не проверяет.
public int calculateDiscount(int price, int percent) {
    if (percent < 0 || percent > 100) {
        // Ошибка программиста/логики. Бросаем unchecked исключение.
        throw new IllegalArgumentException("Процент должен быть от 0 до 100");
    }
    return price * percent / 100;
}
// Вызывающий код может, но не обязан, ловить его.
public void applyDiscount() {
    try {
        calculateDiscount(100, 150); // Выбросит IllegalArgumentException
    } catch (IllegalArgumentException e) {
        System.err.println("Неверные входные данные: " + e.getMessage());
    }
}

Современный подход (с Java 8+): Часто предпочтение отдается unchecked-исключениям, чтобы не загромождать код. Checked-исключения уместны, когда вызывающая сторона действительно может предпринять осмысленные действия для восстановления.