Проверяемые и непроверяемые
В нашем здании есть пожарный инспектор. Он не ждёт пожара, а заранее проверяет план. Для каждой тревоги, которую можно предвидеть, на этаже должен быть человек, который на неё ответит, или табличка «передаём наверх». Без этого здание не примут.
В Java роль инспектора играет компилятор. Метод load ниже загружает файл. Настоящая программа проверила бы диск, а мы для примера считаем, что файла missing.txt нет:
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");
}
}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 из прошлых уроков компилировалось без всякого плана. Почему для одних исключений план нужен, а для других нет? Ответ — в дереве классов исключений.
Дерево исключений
Классы исключений связаны наследованием, как любые другие классы:
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 и класс исключения. Это табличка «передаём наверх»: метод сам на тревогу не отвечает и предупреждает каждого, кто его вызовет.
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 ловит исключение:
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:
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прячет ошибку: пойманное исключение нужно обработать.