Исключения в Java
Обзор исключений в 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. Компилятор не заставляет вас их обрабатывать, потому что в корректном коде их быть не должно.- Проверяемое (checked)
Exception(всё остальное подException) — устранимые сбои, которые программа должна предвидеть.IOException,SQLException. Компилятор требует, чтобы вы их либо поймали, либо объявили в предложенииthrowsвашего метода.
Граница между «ошибкой программирования» (runtime-исключение) и «предвиденным сбоем» (checked-исключение) — одно из самых спорных проектных решений Java. Мы вернёмся к этому в Проверяемые и непроверяемые исключения Java.
Как работают выбрасывание и перехват
Когда выполняется throw, JVM:
- Разматывает стек — текущий метод резко завершается, затем то же делает его вызывающий, и так далее.
- На каждом кадре она проверяет блок
try { ... } catch (SomeType e) { ... }, чейcatchсоответствует типу исключения (или его супертипу). - Первый подходящий
catchпобеждает. Управление переходит туда, стек прекращает разматываться, и выполнение возобновляется в блоке catch. - Если ничего не подходит, поток умирает. В однопоточной программе это означает, что JVM печатает трассировку стека и завершается.
Вот почему выброшенное исключение может пройти через множество методов, прежде чем будет поймано, — каждый непойманный бросок всплывает на ещё один кадр вверх.
Первая проба
Вам не обязательно писать throw, чтобы столкнуться с исключением — Java сама их выбрасывает, когда что-то идёт не так. Вот простейший пример: деление целого на ноль. JVM создаёт для вас ArithmeticException.
Без try/catch третья итерация обрушила бы всю программу, а четвёртая никогда бы не выполнилась. С ним сбой ограничен: мы получаем одно сообщение об ошибке, и цикл продолжается. Эта способность — ограничить ущерб от сбоя — и есть весь смысл механики исключений.
Что охватывает эта часть книги
Остальные главы этой части — рабочий словарь обработки исключений Java, по одной части за раз:
- Облик
try/catch/finallyи для чего нужна каждая часть. - Несколько catch и сокращение multi-catch.
try-with-resourcesдля всего, что нужно закрывать.- Выбрасывание исключений самостоятельно с
throwи их объявление сthrows. - Проверяемые и непроверяемые: какое и когда использовать.
- Полная иерархия классов.
- Написание собственных типов исключений для вашей предметной области.
- Принципы, отличающие хорошую обработку исключений от защитного шума.
Читайте по порядку — каждая глава предполагает предыдущую.
Что дальше
Обработка исключений начинается с самой фундаментальной конструкции: блока try/catch. Продолжайте к Java try...catch.
Practice
A method `loadConfig()` reads a file and throws `IOException` on failure. The caller wraps the call in `try { loadConfig(); } catch (RuntimeException e) { ... }`. The IO fails. What happens?