Checked и unchecked исключения
Разбираем вопрос, который любят задавать почти на каждом собеседовании по Java: почему одни исключения нужно объявлять, а другие — нет.
Исключение (exception) — это сигнал о том, что во время выполнения программы произошло что-то нештатное: файл не найден, деление на ноль, обращение к несуществующему элементу массива. Java не роняет программу молча, а «выбрасывает» специальный объект-исключение, который можно поймать и обработать.
Что выведет этот код?
Начнём с вопроса-ловушки. Посмотрите на этот метод и подумайте, скомпилируется ли он:
import java.io.FileReader;
public class Main {
public static void main(String[] args) {
FileReader reader = new FileReader("data.txt");
System.out.println("Файл открыт");
}
}Если вы пишете на Java хотя бы неделю, вы уже почувствовали подвох. Этот код не скомпилируется. Компилятор выдаст ошибку:
Вывод:
error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader reader = new FileReader("data.txt");
^Конструктор FileReader может выбросить FileNotFoundException — а это исключение относится к особой категории, которую Java требует явно обрабатывать. Вот с этой категории и начнём.
Два лагеря исключений
В Java все исключения делятся на два больших лагеря, и это деление — не формальность, а способ заставить разработчика думать о рисках заранее.
Checked-исключения (проверяемые) — это ошибки, которые компилятор ЗАСТАВЛЯЕТ вас либо поймать через try/catch, либо «передать дальше» через ключевое слово throws в сигнатуре метода. Логика Java простая: если ошибка связана с внешним миром — файлами, сетью, базой данных — вы обязаны заранее подумать, что будете делать, если что-то пойдёт не так. Примеры: IOException, SQLException, FileNotFoundException.
Unchecked-исключения (непроверяемые) — это ошибки программиста, а не внешнего мира. Компилятор НЕ требует их объявлять или ловить. Логика такая: если вы разыменовали null или вышли за границы массива — это баг в вашей логике, а не непредвиденное обстоятельство, с которым можно «работать». Примеры: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException.
Исправим наш пример двумя способами — сначала через try/catch:
import java.io.FileReader;
import java.io.FileNotFoundException;
public class Main {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("data.txt");
System.out.println("Файл открыт");
} catch (FileNotFoundException e) {
System.out.println("Файл не найден: " + e.getMessage());
}
}
}А теперь через throws — мы не обрабатываем исключение сами, а «перекладываем» ответственность на того, кто вызовет наш метод:
import java.io.FileReader;
import java.io.FileNotFoundException;
public class Main {
public static void main(String[] args) throws FileNotFoundException {
FileReader reader = new FileReader("data.txt");
System.out.println("Файл открыт");
}
}Оба варианта компилируются. Разница в том, что во втором случае, если файла не окажется, программа аварийно завершится с трассировкой стека — обработку переложили на вызывающий код (в данном случае это JVM, которая запускает main).
Как это устроено под капотом
В основе — иерархия классов, и это ключ к пониманию, почему одни исключения checked, а другие нет.
| Класс | Категория | Проверяется компилятором? |
Throwable | корень всей иерархии | — |
Error | критические сбои среды (нехватка памяти и т.п.) | нет |
Exception (кроме RuntimeException) | checked | да |
RuntimeException и его наследники | unchecked | нет |
Правило простое: класс RuntimeException и всё, что от него наследуется, — unchecked. Всё остальное, что наследуется от Exception напрямую, — checked. А Error — это вообще отдельная история: такие ошибки сигнализируют о проблемах уровня JVM (например, OutOfMemoryError), их ловить и «чинить» в обычном коде почти никогда не имеет смысла.
Компилятор Java устроен так, что на этапе сборки анализирует, какие checked-исключения может выбросить каждый вызываемый метод, и требует, чтобы вы либо поймали их, либо явно расписались в throws, что «прокидываете» их выше. Для unchecked-исключений такого анализа нет — компилятору всё равно, обработали вы NullPointerException или нет, потому что теоретически он может возникнуть в почти любой строке кода.
Когда это помогает, а когда раздражает
Идея checked-исключений красивая: заставить разработчика не забывать про сетевые обрывы и битые файлы. На практике у неё есть обратная сторона, и об этом тоже стоит знать для собеседования.
Помогает, когда исключение — это ожидаемая часть работы с внешней системой, и вызывающий код реально может что-то сделать: повторить попытку, показать сообщение пользователю, переключиться на резервный сервер.
Раздражает, когда приходится писать try/catch или throws просто чтобы код скомпилировался, хотя обработать проблему всё равно нечем — и разработчики от бессилия начинают писать код вроде такого:
try {
riskyOperation();
} catch (Exception e) {
// тут пусто, потому что «а что я могу сделать»
}Это одна из самых частых претензий к checked-исключениям в реальных проектах, и именно поэтому многие современные библиотеки (например, Spring) сознательно оборачивают checked-исключения в unchecked, чтобы не заставлять весь код «прокидывать» throws по цепочке из десятков методов.
Частые ошибки на собеседовании
- Путают
ErrorиException— говорят, чтоErrorтоже checked. Нет,Errorвообще не про «обработать», это сигнал о серьёзной проблеме среды. - Считают, что
RuntimeExceptionнельзя поймать черезtry/catch. Можно — просто компилятор не заставляет это делать. - Забывают, что
NullPointerException,ArrayIndexOutOfBoundsExceptionиArithmeticException— все unchecked, потому что это баги логики, а не проблемы внешнего мира. - Не могут объяснить, зачем вообще нужно деление на checked/unchecked, и отвечают только «потому что так в Java» — интервьюеры ждут именно понимания компромисса «безопасность vs многословность».
Итоги-шпаргалка
- Checked-исключения (наследники
Exception, кромеRuntimeException) компилятор заставляет ловить или объявлять черезthrows. - Unchecked-исключения (наследники
RuntimeException) — это баги программиста, компилятор их не проверяет. Error— вообще отдельная категория, сигнал о серьёзных проблемах JVM, а не бизнес-логики.- Checked-исключения хороши для ожидаемых сбоев внешнего мира, но при неаккуратном использовании превращаются в пустые
catch-блоки и раздражающий шум в коде.