Checked и Unchecked исключения в Java
Разница между checked и unchecked (runtime) исключениями в Java и когда применять каждое из них.
Java делит исключения на две группы, и компилятор обрабатывает их по-разному. Checked-исключения должны быть пойманы или объявлены; компилятор откажется собирать код, который их игнорирует. Unchecked-исключения не имеют такого требования — компилятор позволяет им распространяться молча, и только среда выполнения их перехватывает. Это разделение — одно из наиболее характерных и спорных решений Java, и его понимание помогает писать надёжный код.
Что к чему относится
Всё, что является Throwable, попадает в одну из трёх категорий:
Error— катастрофа на уровне JVM. Unchecked. Не перехватывать.RuntimeException(подклассException) и все его подтипы. Unchecked.- Остальные подклассы
Exception. Checked.
Правило звучит так: «RuntimeException и Error вместе со своими подтипами являются unchecked; всё остальное под Throwable — checked». Это различие на уровне иерархии классов, а не флаг конфигурации — нет никакого способа сделать RuntimeException checked или IOException unchecked.
Что означает «checked» в коде
Если тело метода выбрасывает checked-исключение, метод обязан выполнить одно из двух, иначе программа не скомпилируется:
// Option 1: catch it
public void load() {
try {
String text = Files.readString(path); // throws IOException
} catch (IOException e) {
// ...
}
}
// Option 2: declare it
public void load() throws IOException {
String text = Files.readString(path);
}Компилятор проходит по каждому методу и проверяет: для каждого checked-исключения, которое может из него выйти, есть ли охватывающий catch или клаузула throws? Если нет — ошибка компиляции.
Для unchecked-исключений такого требования нет. Вы можете выбросить IllegalArgumentException из любого метода, не трогая сигнатуру, и любой вызывающий код волен поймать его или нет.
Замысел разработчиков
Идея за checked-исключениями такова: некоторые сбои — это ожидаемые результаты операции (файл может не существовать, сеть может быть недоступна), и язык должен заставить вызывающий код думать о них. Unchecked-исключения предназначены для ошибок программирования — разыменование null, выход за пределы массива, недопустимый аргумент — где правильное решение — исправить код, а не добавить try/catch.
Это разграничение звучит чисто. На практике оно нарушается:
- Многоуровневый код умножает объявления. Низкоуровневое
IOExceptionиз загрузчика конфигурации не остаётся там — каждый вызывающий его метод обязан обработать или объявить его. К тому моменту, когда исключение достигает контроллера, в сигнатуре перечислены шесть несвязанных checked-исключений. - Лямбды плохо с ними сочетаются.
Stream.map(this::parseLine)не скомпилируется, еслиparseLineвыбрасывает checked-исключение, потому что методapplyуFunctionего не объявляет. - Оборачивание повсеместно. Распространённый обходной путь — поймать checked-исключение немедленно и перебросить его как runtime-исключение, что нивелирует первоначальный замысел.
Большинство современного Java-кода использует меньше checked-исключений, чем стандартная библиотека — иногда вообще ни одного. Новые фреймворки вроде Spring почти полностью опираются на runtime-исключения именно по этой причине.
Когда что использовать
Разумное практическое правило:
- Используйте checked, когда у вызывающего кода есть реалистичная стратегия восстановления, специфичная для данного сбоя — например, повторная попытка при сетевой ошибке, запасной вариант при ошибке разбора. Заставьте их подумать об этом.
- Используйте unchecked, когда сбой является ошибкой программирования (
IllegalArgumentException,NullPointerException,IllegalStateException) или когда ни один вызывающий код не смог бы разумно восстановиться.
В сомнительных случаях предпочитайте unchecked. Цена неправильного выбора меньше, и вы всегда можете ужесточить это позднее.
Опыт на стороне catch
То, является ли исключение checked, меняет то, как выглядит catch:
// checked: caller must catch or declare
try {
parser.parse(path);
} catch (IOException e) {
// handle
}
// unchecked: caller may catch but doesn't have to
try {
return numbers.get(idx);
} catch (IndexOutOfBoundsException e) {
return -1;
}С точки зрения catch поведенческой разницы нет — оба блока ловят то, что объявляют. Разделение имеет значение только на месте вызова, которое не перехватило исключение: checked-исключение вынудит добавить объявление throws; unchecked тихо распространится дальше.
Распространённая ошибка: перехват Exception
Поскольку Exception является родителем как checked, так и unchecked-типов (кроме Error), catch (Exception e) перехватывает почти всё. Это часто является признаком плохого кода:
try { complexOperation(); }
catch (Exception e) { log("failed"); } // hides bugs and real failures alikeПроблема не в том, что перехватывает — а в том, что перехватывает слишком много. NullPointerException здесь указывает на баг; IOException указывает на реальный восстановимый сбой; RuntimeException из библиотеки, которую вы не контролируете, может быть чем угодно. Обращение с ними одинаково обычно означает, что ни одно из них не обрабатывается должным образом. Предпочитайте перехватывать конкретные типы, для которых у вас есть план.
Рабочий пример
Небольшая программа с двумя методами: один объявляет checked-исключение IOException, другой выбрасывает unchecked-исключение IllegalArgumentException. Компилятор обращается с ними по-разному — только checked вынуждает обработку на месте вызова.
Обратите внимание на три вещи. Удалите throws java.io.IOException из maybeReadFile — файл не скомпилируется. Удалите окружающий try/catch из первого вызова — файл тоже не скомпилируется. Но вокруг финального requirePositive(-7) нет try/catch — и это компилируется нормально, хотя вызов приведёт к аварийному завершению программы. Вот асимметричная обработка в одном экране.
Что дальше
Мы говорили о таких типах, как IOException, RuntimeException, Error — но как они соотносятся друг с другом? Иерархия классов невелика и стоит её запомнить. Продолжайте к иерархии исключений Java.