Исключения в Java
Обзор исключений в Java — что это такое, иерархия исключений и почему важна обработка ошибок.
Исключение — это то, что происходит, когда программа сталкивается с ситуацией, которую не может обработать на текущем пути выполнения: файл не найден, деление числа на ноль, индекс массива выходит за границы. Вместо того чтобы вернуть неверный результат или молча повредить состояние, Java останавливает текущий метод, создаёт объект исключения с описанием произошедшего и начинает поиск кода, способного с этим справиться. Изучение механизма исключений — это изучение того, как Java сообщает вам, что пошло не так, где и что с этим можно сделать.
Что такое исключение на самом деле
Исключение — это обычный Java-объект. Точнее, экземпляр класса, унаследованного от java.lang.Throwable. Когда что-то идёт не так, JVM (или ваш собственный код) создаёт один из таких объектов и бросает его. С этого момента программа следует по иному потоку управления — пока либо блок catch не примет исключение, либо поток не завершится.
Объект содержит информацию: тип (класс — NullPointerException, IOException и т. д.), сообщение (описание на человеческом языке) и трассировку стека (цепочку вызовов, зафиксированную в момент создания исключения). Когда вы читаете исключение в консоли, вы видите именно эти три составляющих.
Exception in thread "main" java.lang.ArithmeticException: / by zero
at calc.Money.divide(Money.java:42)
at calc.App.main(App.java:11)Первая строка — тип и сообщение. Строки с отступом — трассировка стека, самый последний вызов идёт первым.
Почему исключения, а не коды ошибок
Старые языки — классический пример C — сигнализируют об ошибках кодами возврата: каждая функция возвращает int, вы его проверяете, и если значение отрицательное — что-то пошло не так. У этого подхода есть два недостатка:
- Возвращаемое значение можно проигнорировать. Вызывающий код, забывший проверить его, не увидит немедленного сбоя, и ошибка проявится позже в неочевидном месте.
- Путь ошибки засоряет основной путь. Каждая строка реальной работы оборачивается в
if (err) return err;.
Исключения меняют это с точностью до наоборот. По умолчанию необработанное исключение останавливает выполнение, и это заметно. Вы явно выбираете, где его обрабатывать — там, где у вас есть стратегия. Основной путь остаётся чистым; путь восстановления находится в отдельном блоке.
Три вида проблем
Java делит всё, что унаследовано от Throwable, на три категории, и это различие важно, поскольку язык обращается с ними по-разному:
Error— проблемы самой JVM.OutOfMemoryError,StackOverflowError. В обычном коде их не перехватывают. Как правило, ничего полезного сделать нельзя.RuntimeException(подклассException) — программные ошибки, проявляющиеся во время выполнения.NullPointerException,IndexOutOfBoundsException,ClassCastException. Компилятор не требует их обработки, поскольку в корректном коде они не должны возникать.- Проверяемые
Exception(всё остальное подException) — восстанавливаемые сбои, которые программа должна предусматривать.IOException,SQLException. Компилятор требует либо перехватить их, либо объявить в конструкцииthrowsметода.
Граница между «программной ошибкой» (непроверяемое исключение) и «ожидаемым сбоем» (проверяемое исключение) — одно из самых спорных проектных решений Java. Мы вернёмся к этому в Java: проверяемые и непроверяемые исключения.
Как работают бросок и перехват
Когда выполняется throw, JVM:
- Разматывает стек — текущий метод аварийно завершается, затем его вызывающий, и так далее.
- На каждом кадре проверяет наличие блока
try { ... } catch (SomeType e) { ... }, в которомcatchсоответствует типу исключения (или его супертипу). - Первый подходящий
catchпобеждает. Управление переходит туда, разматывание стека прекращается, и выполнение возобновляется в блоке catch. - Если ничего не подходит, поток завершается. В однопоточной программе это означает, что JVM выводит трассировку стека и завершает работу.
Вот почему выброшенное исключение может пройти через множество методов, прежде чем будет перехвачено — каждый непойманный throw поднимается ещё на один кадр.
Первый пример
Чтобы столкнуться с исключением, не нужно писать throw самому — Java выбрасывает их сама, когда что-то идёт не так. Вот простейший пример: деление целого числа на ноль. JVM создаёт ArithmeticException за вас.
Без try/catch третья итерация обрушила бы всю программу, и четвёртая никогда бы не выполнилась. С ним сбой локализован: мы получаем одно сообщение об ошибке, и цикл продолжается. Именно эта способность — ограничивать ущерб от сбоя — и есть главная цель механизма исключений.
Что охватывает эта часть книги
Оставшиеся главы этой части — это рабочий словарь обработки исключений в Java, по одной теме за раз:
- Структура
try/catch/finallyи назначение каждого блока. - Несколько блоков catch и сокращение multi-catch.
try-with-resourcesдля всего, что требует закрытия.- Самостоятельный выброс исключений через
throwи их объявление черезthrows. - Проверяемые и непроверяемые: какое использовать и когда.
- Полная иерархия классов.
- Создание собственных типов исключений для вашей предметной области.
- Принципы, отличающие хорошую обработку исключений от защитного шума.
Читайте по порядку — каждая глава предполагает знание предыдущей.
Что дальше
Обработка исключений начинается с самой фундаментальной конструкции: блока try/catch. Перейдите к Java try...catch.