Перейти к содержимому
Теория · Исключения · урок 6 из 12

Проверяемые и непроверяемые

В нашем здании есть пожарный инспектор. Он не ждёт пожара, а заранее проверяет план. Для каждой тревоги, которую можно предвидеть, на этаже должен быть человек, который на неё ответит, или табличка «передаём наверх». Без этого здание не примут.

В Java роль инспектора играет компилятор. Метод load ниже загружает файл. Настоящая программа проверила бы диск, а мы для примера считаем, что файла missing.txt нет:

JAVA
public class Main {
    static void load(String name) {
        if (name.equals("missing.txt")) {
            throw new Exception("no such file: " + name); // ✗
        }
        System.out.println("Loaded " + name);
    }

    public static void main(String[] args) {
        load("notes.txt");
    }
}
TEXT
error: unreported exception Exception; must be caught or declared to be thrown

Строка с ошибкой бросает исключение сама. new Exception("no such file: " + name) создаёт объект класса Exception с сообщением, а оператор throw бросает его — так же, как JVM бросала ArithmeticException.

Программа не компилируется, хотя вызов load("notes.txt") может так и не бросить это исключение. Компилятор говорит: исключение Exception не обработано, его нужно поймать или объявить. Инспектору неважно, случится ли пожар, — он проверяет план.

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

Дерево исключений

Классы исключений связаны наследованием, как любые другие классы:

TEXT
Throwable
├── Error
│   └── StackOverflowError
└── Exception
    ├── RuntimeException
    │   ├── ArithmeticException
    │   ├── NullPointerException
    │   ├── IllegalArgumentException
    │   │   └── NumberFormatException
    │   └── …
    └── IOException

Что означает каждая ветка:

  • Throwable — корень: всё, что можно бросить через throw;
  • Error — сбои самой JVM, например StackOverflowError при слишком глубокой рекурсии; их обычно не ловят: программе с ними не справиться;
  • Exception — ситуации, на которые программа может ответить;
  • RuntimeException — ветка внутри Exception для ошибок в самом коде: деления на ноль, null, неверного аргумента;
  • IOException — ошибки ввода-вывода: файла нет на диске, оборвалась сеть.

Все знакомые вам исключения, от ArithmeticException до NumberFormatException, растут из ветки RuntimeException.

Две группы исключений

Проверяемые исключения — Exception и его наследники, кроме ветки RuntimeException. Для них компилятор требует план: каждое нужно поймать или объявить. Это ситуации, которые от программиста не зависят, как пропавший файл, поэтому к ним надо быть готовым заранее. Настоящий метод загрузки бросил бы IOException.

Непроверяемые исключения — ветки RuntimeException и Error. Для них компилятор ничего не требует. Они говорят об ошибке в коде, и правильный ответ на них — исправить код: не делить на ноль, проверять null.

Разницу между проверяемыми и непроверяемыми исключениями спрашивают почти на каждом собеседовании по Java. Типичный неверный ответ — «RuntimeException проверяемое, для него нужен throws». Проверяемость решает место класса в дереве, а не то, насколько серьёзна ошибка.

Поймать или объявить

Если внутри метода может вылететь проверяемое исключение, у метода два пути.

Поймать

Обернуть опасный код в try с подходящим catch. Тогда исключение не выйдет из метода, и тем, кто его вызывает, ничего делать не нужно.

Объявить через throws

Написать в заголовке метода слово throws и класс исключения. Это табличка «передаём наверх»: метод сам на тревогу не отвечает и предупреждает каждого, кто его вызовет.

JAVA
static void load(String name) throws Exception {
    if (name.equals("missing.txt")) {
        throw new Exception("no such file: " + name);
    }
    System.out.println("Loaded " + name);
}

Теперь план нужен тому, кто вызывает load. Метод main ловит исключение:

JAVA
try {
    load("notes.txt"); // Loaded notes.txt
    load("missing.txt");
} catch (Exception e) {
    System.out.println(e.getMessage()); // no such file: missing.txt
}

У main есть и второй путь — тоже объявить: public static void main(String[] args) throws Exception. Тогда исключение, дойдя до main, остановит программу с трассировкой стека, как непойманное.

Здесь картинка с инспектором неточна. Настоящему инспектору хватит одного ответственного на всё здание. Компилятор требует решения на каждом этаже, через который исключение может пройти: каждый метод либо ловит его, либо объявляет throws.

Частая ошибка

Чтобы компилятор замолчал, исключение ловят пустым catch:

JAVA
try {
    load("missing.txt");
} catch (Exception e) { // ✗
}
System.out.println("Done");

Программа компилируется и печатает Done, как будто всё в порядке. Но файл не загрузился, а сообщение no such file: missing.txt пропало без следа. Пустой catch — это человек, который выключил сигнализацию и ушёл. Если поймали исключение, ответьте на него: хотя бы напечатайте e.getMessage(), а лучше исправьте ситуацию.

Главное

  • Throwable делится на Error и Exception, а внутри Exception есть ветка RuntimeException.
  • Проверяемые исключения — Exception без ветки RuntimeException; непроверяемые — RuntimeException, Error и их наследники.
  • Проверяемое исключение метод обязан поймать в try/catch или объявить через throws, иначе программа не скомпилируется.
  • Пустой catch прячет ошибку: пойманное исключение нужно обработать.