Исключение != ошибка
Время на прочтение
4 мин
Количество просмотров 27K
Многие программисты почему-то считают, что исключения и ошибки — это одно и то же. Кто-то постоянно кидает exception, кто-то через errorHandler превращает ошибки в исключения. Некоторые пытаются увеличить производительность, используя исключения. Но, на самом деле, exception и ошибки — это совершенно разные механизмы. Не надо одним механизмом заменять другой. Они созданы для разных целей.
Когда появился php5 с исключениями, а затем ZendFramework, который всегда кидает исключения — я не мог понять: чем же exception лучше моего любимого trigger_error()? Долго думал, обсуждал с коллегами и разобрался в этом вопросе. Теперь я чётко знаю, где использовать trigger_error(), а где throw new Exception().
В чём же принципиальная разница между ними?
Ошибки
Ошибки — это то, что нельзя исправить, об этом можно только сообщить: записать в лог, отправить email разработчику и извинится перед пользователем. Например, если мой движок не может подключиться к БД, то это ошибка. Всё. Точка. Без БД сайт не работает, и я не могу с этим ничего сделать. Поэтому я вызываю ales_kaput() и trigger_error(), а мой errorHandler отправит мне email и покажет посетителю сообщение «Извините, сайт не работает».
Exception
Исключения — это не ошибки, это всего лишь особые ситуации, которые нужно как-то обработать. Например, если в калькуляторе вы попробуете разделить на ноль, то калькулятор не зависнет, не будет отсылать сообщения разработчику и извинятся перед вами. Такие ситуации можно обрабатывать обычным if-ом. Строго говоря, исключения — это конструкция языка позволяющая управлять потоком выполнения. Это конструкция, стоящая в одном ряду с if, for и return. И всё. Этот механизм ничем более не является. Только управление потоком.
Их основное предназначение: пробрасывать по каскаду. Покажу это на примере: есть три функции, которые вызывают друг друга каскадом:
<?php
a();
function a()
{
b();
}
function b()
{
c(99);
}
function c($x)
{
if ($x === 0) {
// Некоторая особенная ситуация,
// которая должна остановить выполнение функций c() и b(),
// а функция a() должна узнать об этом
}
return 100 / $x;
}
Эту задачу можно было бы решить без механизма exception. Например, можно заставить все функции возвращать специальный тип (если ты матёрый пэхапэшник, то должен вспомнить PEAR_Error). Для простоты я обойдусь null-ом:
<?php
a();
function a()
{
echo 'a-begin';
$result = b();
if ($result === null) {
echo 'Делить на ноль нехорошо';
return;
}
echo 'a-stop';
}
function b()
{
echo 'b-begin';
$result = c(0);
if ($result === null) {
return null;
}
echo 'b-stop';
return true;
}
function c($x)
{
echo 'c-begin';
if ($x === 0) {
return null;
}
echo 'c-stop';
return 100 / $x;
}
Результат работы:
a-begin
b-begin
c-begin
Делить на ноль нехорошо
Задача выполнена, но, обратите внимание, мне пришлось модифицировать промежуточную функцию b(), чтобы она пробрасывала результат работы нижестоящей функции выше по каскаду. А если у меня каскад из 5 или 10 функций? То мне пришлось бы модифицировать ВСЕ промежуточные функции. А если исключительная ситуация в конструкторе? То мне пришлось бы подставлять костыли.
А теперь решение с использованием Exception:
a();
function a()
{
echo 'a-begin';
try {
b();
echo 'a-stop';
} catch (Exception $e) {
echo $e->getMessage();
}
}
function b()
{
echo 'b-begin';
c(0);
echo 'b-stop';
}
function c($x)
{
echo 'c-begin';
if ($x === 0) {
throw new Exception('Делить на ноль нехорошо');
}
echo 'c-stop';
return 100 / $x;
}
Результат выполнения будет идентичен. Функция b() осталась в первоначальном виде, не тронутая. Это особенно актуально, если у вас длинные каскады. И ещё объект $e может содержать дополнительную информацию о произошедшей ситуации.
Таким образом, получается, что ошибки и исключения — это совершенно разные инструменты для решения совершенно разных задач:
ошибка — не поправимая ситуация;
исключение – позволяет прервать выполнение каскада функций и пробросить некоторую информацию. Что-то вроде глобального оператора return. Если у Вас нет каскада, то вам достаточно использовать if или return.
Ошибки не всегда являются ошибками
Некоторые могут мне возразить: «Посмотри в Zend Framework — там всегда кидают исключения. Это best practics, и надо делать также. Даже если не удалось подключиться к БД, надо кидать исключение».
В этой статье я как раз хочу развеять это заблуждение. Zend действительно является best practics, но программисты Зенда находятся на другой лодке и делают другие вещи. Принципиальная разница между ними и мной в том, что они пишут универсальную библиотеку, которая будет использоваться во многих проектах. И они со своей колокольни не могут сказать, что является критической ошибкой, а что является поправимой.
Например, в вашем проекте может быть несколько MySQL серверов и вы можете переключаться между ними при падении одного из них. По этому, Zend_Db, как универсальная библиотека, кидает исключение, а что с ним делать — решайте сами. Exception это гибко — вы сами решаете на каком уровне и какой тип ситуаций ловить. Вы можете вывести сообщение об ошибке или попытаться исправить возникшую ситуацию, если знаете как. При написании универсальных библиотек необходимо всегда кидать исключения. Это делает библиотеку более гибкой.
В итоге, могу сказать, что у обоих механизмов есть свои особенности и, самое главное, что у них есть своё предназначение и эти предназначения нисколько не пересекаются.
Ошибки != исключения
. Не надо использовать исключения для улучшения быстродействия или сообщения об ошибках. Не надо в классе My_Custom_Exception реализовывать какую-либо логику исправления ситуации. Этот класс должен быть пустым, он создаётся только что бы определить тип ситуации и поймать только то, что надо. Название класса ‘My_Custom_Exception’ это такой древовидный аналог линейному списку констант E_*** (E_NOTICE, E_WARNING, …).
В php давно был разработан механизм обработки ошибок, и он отлично работает. Я им отлично пользуюсь там, где это надо.
I intend to give you a most unusual discussion of error control.
I built a very good error handler into a language years ago, and though some of the names have changed, the principles of error processing are the same today. I had a custom built multi-tasking OS and had to be able to recover from data errors at all levels with no memory leaks, stack growth or crashes. So what follows is my understanding of how errors and exceptions must operate and how they differ. I will just say I do not have an understanding of how the internals of try catch works, so am guessing to some measure.
The first thing that happens under the covers for error processing is jumping from one program state to another. How is that done? I’ll get to that.
Historically, errors are older and simpler, and exceptions are newer and a bit more complex and capable. Errors work fine until you need to bubble them up, which is the equivalent of handing a difficult problem to your supervisor.
Errors can be numbers, like error numbers, and sometimes with one or more associated strings. For example if a file-read error occurs you might be able to report what it is and possibly gracefully fail. (Hay, it’s a step up from just crashing like in the old days.)
What is not often said about exceptions is that exceptions are objects layered on a special exception stack. It’s like a return stack for program flow, but it holds a return state just for error trys and catches. (I used to call them ePush and ePop, and ?Abort was a conditional throw which would ePop and recover to that level, while Abort was a full die or exit.)
On the bottom of the stack is the information about the initial caller, the object that knows about the state when the outer try was started, which is often when your program was started. On top that, or the next layer on the stack, with up being the children, and down being the parents, is the exception object of the next inner try/catch block.
If you put a try inside a try you are stacking the inner try on top of the outer try. When an error occurs in the inner try and either the inner catch can’t handle it or the error is thrown to the outer try, then control is passed to the outer catch block (object) to see if it can handle the error, i.e. your supervisor.
So what this error stack really does is to be able to mark and restore program flow and system state, in other words, it allows a program to not crash the return stack and mess up things for others (data) when things go wrong. So it also saves the state of any other resources like memory allocation pools and so it can clean them up when catch is done. In general this can be a very complicated thing, and that is why exception handling is often slow. In general quite a bit of state needs to go into these exception blocks.
So a try/catch block sort of sets a state to be able to return to if all else gets messed up. It’s like a parent. When our lives get messed up we can fall back into our parent’s lap and they will make it all right again.
Hope I didn’t disappoint you.
Исключения в программировании (exceptions) — это механизм, который позволяет программе обрабатывать нетипичную ситуацию и при этом не прекращать работу. Благодаря этому механизму разработчик может описать в коде реакцию программы на такие ситуации.
Простой пример: в программе-калькуляторе исключением может стать ситуация, когда пользователь решит поделить на ноль. Это не должно стать ошибкой, из-за которой рушится вся программа, но чтобы ситуация не застопорила исполнение остального кода, нужно ее правильно обработать. Для этого необходимы обработчики исключений. Они позволяют «сказать» программе, что ей делать, если такое случится.
Механизм обработки исключений существует в большинстве языков программирования. Он может быть реализован немного по-разному, но общая суть схожа: это всегда какие-то особые случаи, которые надо обработать отдельно. Мы при описании будем отталкиваться от особенностей исключений в Java, но встретить их можно и в других языках: JavaScript, PHP, Python, C++ и так далее.
Зачем нужны исключения
Механизм обработки исключений может понадобиться любому разработчику. Если не отслеживать исключительные ситуации, может возникнуть незаметная ошибка, которая нарушит работу всего кода, или программа может «зависнуть» либо «упасть» — потому что сложный момент не был обработан как надо.
Исключения нужны, чтобы программа продолжала относительно корректно работать, даже если что-то пошло не так.
Какими бывают исключения
Исключения делятся на две большие группы, которые пересекаются друг с другом: синхронные и асинхронные. Синхронные могут возникнуть только в конкретном месте программы или при выполнении определенной операции: открытие файла, деление и так далее. Асинхронные могут возникнуть когда и где угодно. Их «ловят» по-разному, чтобы успешно отслеживать и те, и другие.
Мы сказали, что эти группы пересекаются друг с другом, хотя по логике они противоположны. Пересечение происходит потому, что при выполнении операций асинхронным может стать даже формально синхронное исключение, и наоборот.
Как происходит работа с исключениями
- Разработчик пишет код и понимает, что в какой-то момент в том или ином месте может возникнуть нештатная ситуация. Бывает, что исключения добавляют в уже написанный код — например, нештатную ситуацию обнаружили при тестировании.
- В этом месте пишется особый блок кода — обработчик. Он говорит программе: здесь может возникнуть особая ситуация, если она случится, выполни вот это.
- Внутри обработчика — функция, которая выполнится, если программа столкнется с описанной ситуацией. Она или исправит ситуацию, или скорректирует дальнейшее выполнение программы.
Бывают исключения, которые нельзя предусмотреть. Разработчики обрабатывают не все возможные нештатные ситуации, а только самые очевидные, чтобы не перегружать код. Это справедливо для большинства сфер разработки, кроме тех, где слишком высока цена ошибки.
Как устроена обработка исключений
Существуют разные виды обработки: структурная и неструктурная, с возвратом и без возврата. Они различаются механизмом действия, но общая суть одна: это функция, которая запускается, если в коде случилась та или иная исключительная ситуация. Тут можно использовать условный оператор if или специальные синтаксические конструкции.
В примере с делением на ноль обработчик может отменить попытку деления и сказать пользователю, что на ноль делить нельзя, — но это самый простой пример. В реальности все сложнее.
Обработка с возвратом и без возврата. Эти виды обработки различаются реакцией на случившееся исключение. Версия с возвратом предполагает, что обработчик попытается разрешить проблему, а когда ему это удастся, вернет программу к исходному поведению. В итоге она будет работать так, как если бы исключения не возникало.
Вот пример: не запустился скрипт, необходимый для работы следующего скрипта. Следующий скрипт заметил это, зафиксировал исключение и обратился к обработчику, который запустил нужный скрипт «вручную». После этого все может работать, как и было задумано.
Обработка без возврата — вид обработки, когда проблема не ликвидируется, а участок кода, который не получается выполнить, пропускается. В примере со скриптами обработка «переключила» бы выполнение кода на момент, где уже не понадобится незаработавший скрипт.
Структурная и неструктурная обработка. Это два способа подключить обработчики. В первом случае они встраиваются в код, а когда генерируется исключение, для него выбирается тот или иной обработчик в зависимости от ситуации. Во втором случае обработчики существуют отдельно и «подключаются» к конкретным видам исключений с помощью специальных команд. Способ выбирается в зависимости от вида исключения, особенностей кода и языка.
Обычно асинхронные исключения обрабатывают неструктурно, а синхронные — структурно.
Гарантированное завершение. Так называется отдельный вид функции, которая обычно пишется после обработчика. Она описывает действия, которые должны произойти в этой части кода вне зависимости от того, произошло исключение или нет.
Исключения и ошибки: разница
Кроме исключений, в языках программирования существует механизм обработки ошибок. Их часто путают, особенно новички. И то, и другое подразумевает нетипичную ситуацию, в которой работу программы нельзя продолжить корректно. Но есть и различия:
- ошибка означает, что программа «упала», что ее работу нельзя продолжить и она должна быть завершена. Ошибку невозможно исправить — только сообщить о ней пользователю, записать в лог и прекратить исполнение кода;
- исключение — это нештатная ситуация, которую тем не менее можно попробовать починить «на ходу», не закрывая программу. В этом есть смысл, в отличие от ситуации с ошибкой.
Это действительно похожие понятия. В Java, например, сущности исключений и ошибок наследуются от общего предка — интерфейса Throwable. Но ошибка — это явление, когда что-то сделать принципиально не получается. А исключение — ситуация, когда программа просто не знает, что делать, если не указать на это дополнительно.
Можно провести аналогию. Мама послала дочь в магазин за покупками и сказала ей купить батон хлеба. Если хлеба в магазине не оказалось, девочка не сможет его купить. Это ошибка. А если в магазине есть три вида батонов, или все батоны вчерашние, а девочка не знает, нужен ли маме только свежий хлеб, или батон есть, но только из ржаной муки, — это исключения.
В первом случае дочь просто вернется домой и ничего не купит. Из-за ошибки программа не выполняется. Во втором случае девочка позвонит маме и спросит, что ей делать. Программа передаст управление обработчику, чтобы тот разрешил сложную ситуацию.
Когда пользоваться исключениями, а когда — ошибками
В некоторых случаях разработчики описывают все нештатные ситуации как исключения. Например, при создании новых библиотек, которые должны быть очень гибкими и подразумевать многие ситуации — то, что критично для одной задачи, окажется поправимым в другой. Но это редкие случаи, и чаще приходится выбирать между обработкой ошибки и исключения.
Обработчики ошибок советуют использовать тогда, когда проблема не решаема изнутри программы. Например, у приложения нет связи с сервером — оно не может продолжать работу без этого. Или какие-то критичные файлы оказались повреждены, и из-за этого код просто нельзя исполнить. Или в системе закончилась свободная память. Это никак не поправить программными способами.
Исключениями стоит пользоваться, если возникла нештатная, неправильная ситуация, которую не подразумевает логика работы программы. Но программу при этом не нужно выключать и завершать — надо исправить или «перескочить» проблемный момент и сохранить все остальное.
Как начать пользоваться исключениями
В большинстве языков механизм обработки исключений есть по умолчанию — это популярная функция. Но приступать к работе с ними рекомендуют после изучения базовых возможностей языка. Мы советуем идти от простого к сложному: начать с основ и затем переходить к комплексным темам. Конкретно обработка исключений обычно изучается перед тем, как человек переходит к практическим проектам, потому что любая более-менее сложная программа может столкнуться с исключениями в ходе работы.
Содержание
- 1 Методы обработки ошибок
- 2 Исключения
- 3 Классификация исключений
- 3.1 Проверяемые исключения
- 3.2 Error
- 3.3 RuntimeException
- 4 Обработка исключений
- 4.1 try-catch-finally
- 4.2 Обработка исключений, вызвавших завершение потока
- 4.3 Информация об исключениях
- 5 Разработка исключений
- 6 Исключения в Java7
- 7 Примеры исключений
- 8 Гарантии безопасности
- 9 Источники
Методы обработки ошибок
1. Не обрабатывать.
2. Коды возврата. Основная идея — в случае ошибки возвращать специальное значение, которое не может быть корректным. Например, если в методе есть операция деления, то придется проверять делитель на равенство нулю. Также проверим корректность аргументов a
и b
:
Double f(Double a, Double b) { if ((a == null) || (b == null)) { return null; } //... if (Math.abs(b) < EPS) { return null; } else { return a / b; } }
При вызове метода необходимо проверить возвращаемое значение:
Double d = f(a, b); if (d != null) { //... } else { //... }
Минусом такого подхода является необходимость проверки возвращаемого значения каждый раз при вызове метода. Кроме того, не всегда возможно определить тип ошибки.
3.Использовать флаг ошибки: при возникновении ошибки устанавливать флаг в соответствующее значение:
boolean error; Double f(Double a, Double b) { if ((a == null) || (b == null)) { error = true; return null; } //... if (Math.abs(b) < EPS) { error = true; return b; } else { return a / b; } }
error = false; Double d = f(a, b); if (error) { //... } else { //... }
Минусы такого подхода аналогичны минусам использования кодов возврата.
4.Можно вызвать метод обработки ошибки и возвращать то, что вернет этот метод.
Double f(Double a, Double b) { if ((a == null) || (b == null)) { return nullPointer(); } //... if (Math.abs(b) < EPS) { return divisionByZero(); } else { return a / b; } }
Но в таком случае не всегда возможно проверить корректность результата вызова основного метода.
5.В случае ошибки просто закрыть программу.
if (Math.abs(b) < EPS) { System.exit(0); return this; }
Это приведет к потере данных, также невозможно понять, в каком месте возникла ошибка.
Исключения
В Java возможна обработка ошибок с помощью исключений:
Double f(Double a, Double b) { if ((a == null) || (b == null)) { throw new IllegalArgumentException("arguments of f() are null"); } //... return a / b; }
Проверять b
на равенство нулю уже нет необходимости, так как при делении на ноль метод бросит непроверяемое исключение ArithmeticException
.
Исключения позволяют:
- разделить обработку ошибок и сам алгоритм;
- не загромождать код проверками возвращаемых значений;
- обрабатывать ошибки на верхних уровнях, если на текущем уровне не хватает данных для обработки. Например, при написании универсального метода чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей метод программы;
- классифицировать типы ошибок, обрабатывать похожие исключения одинаково, сопоставлять специфичным исключениям определенные обработчики.
Каждый раз, когда при выполнении программы происходит ошибка, создается объект-исключение, содержащий информацию об ошибке, включая её тип и состояние программы на момент возникновения ошибки.
После создания исключения среда выполнения пытается найти в стеке вызовов метод, который содержит код, обрабатывающий это исключение. Поиск начинается с метода, в котором произошла ошибка, и проходит через стек в обратном порядке вызова методов. Если не было найдено ни одного подходящего обработчика, выполнение программы завершается.
Таким образом, механизм обработки исключений содержит следующие операции:
- Создание объекта-исключения.
- Заполнение stack trace’а этого исключения.
- Stack unwinding (раскрутка стека) в поисках нужного обработчика.
Классификация исключений
Класс Java Throwable
описывает все, что может быть брошено как исключение. Наследеники Throwable
— Exception
и Error
— основные типы исключений. Также RuntimeException
, унаследованный от Exception
, является существенным классом.
Иерархия стандартных исключений
Проверяемые исключения
Наследники класса Exception
(кроме наслеников RuntimeException
) являются проверяемыми исключениями(checked exception). Как правило, это ошибки, возникшие по вине внешних обстоятельств или пользователя приложения – неправильно указали имя файла, например. Эти исключения должны обрабатываться в ходе работы программы, поэтому компилятор проверяет наличие обработчика или явного описания тех типов исключений, которые могут быть сгенерированы некоторым методом.
Все исключения, кроме классов Error
и RuntimeException
и их наследников, являются проверяемыми.
Error
Класс Error
и его подклассы предназначены для системных ошибок. Свои собственные классы-наследники для Error
писать (за очень редкими исключениями) не нужно. Как правило, это действительно фатальные ошибки, пытаться обработать которые довольно бессмысленно (например OutOfMemoryError
).
RuntimeException
Эти исключения обычно возникают в результате ошибок программирования, такие как ошибки разработчика или неверное использование интерфейса приложения. Например, в случае выхода за границы массива метод бросит OutOfBoundsException
. Такие ошибки могут быть в любом месте программы, поэтому компилятор не требует указывать runtime исключения в объявлении метода. Теоретически приложение может поймать это исключение, но разумнее исправить ошибку.
Обработка исключений
Чтобы сгенерировать исключение используется ключевое слово throw
. Как и любой объект в Java, исключения создаются с помощью new
.
if (t == null) { throw new NullPointerException("t = null"); }
Есть два стандартных конструктора для всех исключений: первый — конструктор по умолчанию, второй принимает строковый аргумент, поэтому можно поместить подходящую информацию в исключение.
Возможна ситуация, когда одно исключение становится причиной другого. Для этого существует механизм exception chaining. Практически у каждого класса исключения есть конструктор, принимающий в качестве параметра Throwable
– причину исключительной ситуации. Если же такого конструктора нет, то у Throwable
есть метод initCause(Throwable)
, который можно вызвать один раз, и передать ему исключение-причину.
Как и было сказано раньше, определение метода должно содержать список всех проверяемых исключений, которые метод может бросить. Также можно написать более общий класс, среди наследников которого есть эти исключения.
void f() throws InterruptedException, IOException { //...
try-catch-finally
Код, который может бросить исключения оборачивается в try
-блок, после которого идут блоки catch
и finally
(Один из них может быть опущен).
try { // Код, который может сгенерировать исключение }
Сразу после блока проверки следуют обработчики исключений, которые объявляются ключевым словом catch.
try { // Код, который может сгенерировать исключение } catch(Type1 id1) { // Обработка исключения Type1 } catch(Type2 id2) { // Обработка исключения Type2 }
Сatch
-блоки обрабатывают исключения, указанные в качестве аргумента. Тип аргумента должен быть классом, унаследованного от Throwable
, или самим Throwable
. Блок catch
выполняется, если тип брошенного исключения является наследником типа аргумента и если это исключение не было обработано предыдущими блоками.
Код из блока finally
выполнится в любом случае: при нормальном выходе из try
, после обработки исключения или при выходе по команде return
.
NB: Если JVM выйдет во время выполнения кода из try
или catch
, то finally
-блок может не выполниться. Также, например, если поток выполняющий try
или catch
код остановлен, то блок finally
может не выполниться, даже если приложение продолжает работать.
Блок finally
удобен для закрытия файлов и освобождения любых других ресурсов. Код в блоке finally
должен быть максимально простым. Если внутри блока finally
будет брошено какое-либо исключение или просто встретится оператор return
, брошенное в блоке try
исключение (если таковое было брошено) будет забыто.
import java.io.IOException; public class ExceptionTest { public static void main(String[] args) { try { try { throw new Exception("a"); } finally { throw new IOException("b"); } } catch (IOException ex) { System.err.println(ex.getMessage()); } catch (Exception ex) { System.err.println(ex.getMessage()); } } }
После того, как было брошено первое исключение — new Exception("a")
— будет выполнен блок finally
, в котором будет брошено исключение new IOException("b")
, именно оно будет поймано и обработано. Результатом его выполнения будет вывод в консоль b
. Исходное исключение теряется.
Обработка исключений, вызвавших завершение потока
При использовании нескольких потоков бывают ситуации, когда поток завершается из-за исключения. Для того, чтобы определить с каким именно, начиная с версии Java 5 существует интерфейс Thread.UncaughtExceptionHandler
. Его реализацию можно установить нужному потоку с помощью метода setUncaughtExceptionHandler
. Можно также установить обработчик по умолчанию с помощью статического метода Thread.setDefaultUncaughtExceptionHandler
.
Интерфейс Thread.UncaughtExceptionHandler
имеет единственный метод uncaughtException(Thread t, Throwable e)
, в который передается экземпляр потока, завершившегося исключением, и экземпляр самого исключения. Когда поток завершается из-за непойманного исключения, JVM запрашивает у потока UncaughtExceptionHandler
, используя метод Thread.getUncaughtExceptionHandler()
, и вызвает метод обработчика – uncaughtException(Thread t, Throwable e)
. Все исключения, брошенные этим методом, игнорируются JVM.
Информация об исключениях
-
getMessage()
. Этот метод возвращает строку, которая была первым параметром при создании исключения; -
getCause()
возвращает исключение, которое стало причиной текущего исключения; -
printStackTrace()
печатает stack trace, который содержит информацию, с помощью которой можно определить причину исключения и место, где оно было брошено.
Exception in thread "main" java.lang.IllegalStateException: A book has a null property at com.example.myproject.Author.getBookIds(Author.java:38) at com.example.myproject.Bootstrap.main(Bootstrap.java:14) Caused by: java.lang.NullPointerException at com.example.myproject.Book.getId(Book.java:22) at com.example.myproject.Author.getBookIds(Author.java:35)
Все методы выводятся в обратном порядке вызовов. В примере исключение IllegalStateException
было брошено в методе getBookIds
, который был вызван в main
. «Caused by» означает, что исключение NullPointerException
является причиной IllegalStateException
.
Разработка исключений
Чтобы определить собственное проверяемое исключение, необходимо создать наследника класса java.lang.Exception
. Желательно, чтобы у исключения был конструкор, которому можно передать сообщение:
public class FooException extends Exception { public FooException() { super(); } public FooException(String message) { super(message); } public FooException(String message, Throwable cause) { super(message, cause); } public FooException(Throwable cause) { super(cause); } }
Исключения в Java7
- обработка нескольких типов исключений в одном
catch
-блоке:
catch
(IOException | SQLException ex) {...}
В таких случаях параметры неявно являются final
, поэтому нельзя присвоить им другое значение в блоке catch
.
Байт-код, сгенерированный компиляцией такого catch
-блока будет короче, чем код нескольких catch
-блоков.
-
Try
с ресурсами позволяет прямо вtry
-блоке объявлять необходимые ресурсы, которые по завершению блока будут корректно закрыты (с помощью методаclose()
). Любой объект реализующийjava.lang.AutoCloseable
может быть использован как ресурс.
static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
В приведенном примере в качестве ресурса использутся объект класса BufferedReader
, который будет закрыт вне зависимосити от того, как выполнится try
-блок.
Можно объявлять несколько ресурсов, разделяя их точкой с запятой:
public static void viewTable(Connection con) throws SQLException { String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query)) { //Work with Statement and ResultSet } catch (SQLException e) { e.printStackTrace; } }
Во время закрытия ресурсов тоже может быть брошено исключение. В try-with-resources добавленна возможность хранения «подавленных» исключений, и брошенное try
-блоком исключение имеет больший приоритет, чем исключения получившиеся во время закрытия. Получить последние можно вызовом метода getSuppressed()
от исключения брошенного try
-блоком.
- Перебрасывание исключений с улучшенной проверкой соответствия типов.
Компилятор Java SE 7 тщательнее анализирует перебрасываемые исключения. Рассмотрим следующий пример:
static class FirstException extends Exception { } static class SecondException extends Exception { } public void rethrowException(String exceptionName) throws Exception { try { if ("First".equals(exceptionName)) { throw new FirstException(); } else { throw new SecondException(); } } catch (Exception ex) { throw e; } }
В примере try
-блок может бросить либо FirstException
, либо SecondException
. В версиях до Java SE 7 невозможно указать эти исключения в декларации метода, потому что catch
-блок перебрасывает исключение ex
, тип которого — Exception
.
В Java SE 7 вы можете указать, что метод rethrowException
бросает только FirstException
и SecondException
. Компилятор определит, что исключение Exception ex
могло возникнуть только в try
-блоке, в котором может быть брошено FirstException
или SecondException
. Даже если тип параметра catch
— Exception
, компилятор определит, что это экземпляр либо FirstException
, либо SecondException
:
public void rethrowException(String exceptionName) throws FirstException, SecondException { try { // ... } catch (Exception e) { throw e; } }
Если FirstException
и SecondException
не являются наследниками Exception
, то необходимо указать и Exception
в объявлении метода.
Примеры исключений
- любая операция может бросить
VirtualMachineError
. Как правило это происходит в результате системных сбоев. -
OutOfMemoryError
. Приложение может бросить это исключение, если, например, не хватает места в куче, или не хватает памяти для того, чтобы создать стек нового потока. -
IllegalArgumentException
используется для того, чтобы избежать передачи некорректных значений аргументов. Например:
public void f(Object a) { if (a == null) { throw new IllegalArgumentException("a must not be null"); } }
IllegalStateException
возникает в результате некорректного состояния объекта. Например, использование объекта перед тем как он будет инициализирован.
Гарантии безопасности
При возникновении исключительной ситуации, состояния объектов и программы могут удовлетворять некоторым условиям, которые определяются различными типами гарантий безопасности:
- Отсутствие гарантий (no exceptional safety). Если было брошено исключение, то не гарантируется, что все ресурсы будут корректно закрыты и что объекты, методы которых бросили исключения, могут в дальнейшем использоваться. Пользователю придется пересоздавать все необходимые объекты и он не может быть уверен в том, что может переиспозовать те же самые ресурсы.
- Отсутствие утечек (no-leak guarantee). Объект, даже если какой-нибудь его метод бросает исключение, освобождает все ресурсы или предоставляет способ сделать это.
- Слабые гарантии (weak exceptional safety). Если объект бросил исключение, то он находится в корректном состоянии, и все инварианты сохранены. Рассмотрим пример:
class Interval { //invariant: left <= right double left; double right; //... }
Если будет брошено исключение в этом классе, то тогда гарантируется, что ивариант «левая граница интервала меньше правой» сохранится, но значения left
и right
могли измениться.
- Сильные гарантии (strong exceptional safety). Если при выполнении операции возникает исключение, то это не должно оказать какого-либо влияния на состояние приложения. Состояние объектов должно быть таким же как и до вызовов методов.
- Гарантия отсутствия исключений (no throw guarantee). Ни при каких обстоятельствах метод не должен генерировать исключения. В Java это невозможно, например, из-за того, что
VirtualMachineError
может произойти в любом месте, и это никак не зависит от кода. Кроме того, эту гарантию практически невозможно обеспечить в общем случае.
Источники
- Обработка ошибок и исключения — Сайт Георгия Корнеева
- Лекция Георгия Корнеева — Лекториум
- The Java Tutorials. Lesson: Exceptions
- Обработка исключений — Википедия
- Throwable (Java Platform SE 7 ) — Oracle Documentation
- try/catch/finally и исключения — www.skipy.ru
«Исключение» — сокращение от слов «исключительный случай». Это ситуация, в которой программа не может продолжить работу или её работа становится бессмысленной.
Исключения бывают и намеренными. Такие исключения разработчик вызывает (бросает, выбрасывает) сам.
Исключение отличается от ошибок. В документации Java сказано:
Error is the superclass of all the exceptions from which ordinary programs are not ordinarily expected to recover.
Ошибки (Error) — это такие исключительные ситуации, в которых восстанавливать работу программы не предполагается.
Например, если вернуться к предыдущей теме, когда не задано базовое условие выхода из рекурсии, переполняется стэк (stack overflow). В этом случае можно только прекратить работу программы.
В случае с исключением можно сделать так, что программа продолжит работу и пользователь: либо ничего не увидит, либо поймёт, что сделал что-то не верно.
Исключение: что с ним делать?
Когда возникает исключение, программисты говорят:
- код (вы)бросил исключение;
- код (вы)кинул исключение.
Это и не удивительно. Ведь все исключения – наследники класса Throwable – бросаемый, или тот, который можно бросить.
Знакомство начнём с семейства Exception, что так и переводится как «исключение».
Встроенное исключение
Некоторые встроенные исключения:
- ArithmeticException: возникает при делении на ноль;
- ArrayIndexOutOfBoundException: индекс вне границ массива;
- IllegalArgumentException: использование неверного аргумента при вызове метода;
- NumberFormatException: ошибка преобразования строки в число.
Рассмотрим их отдельно на примерах, создавая методы и вызывая их (мы уже умеем это делать). По традиции, начнём с деления на ноль.
Деление на ноль (перехвата нет)public class Program { // Ваш класс может называться по-другому
public static void main(String args[]) {
System.out.println("Divide without problem: " + divideTwoNum(42, 2));
System.out.println("Here the problem: " + divideTwoNum(42, 0));
}
static int divideTwoNum(int a, int b) {
return a / b; // возвращаем результат деления
}
}
Вывод будет:
Divide without problem: 21
Exception in thread "main" java.lang.ArithmeticException: / by zero
Вывода: Here the problem… и вовсе не было. Кроме того, программа аварийно завершилась. Теперь перепишем так:
"Перехват" деления на нольpublic class Program { // Ваш класс может называться по-другому
public static void main(String args[]) {
System.out.println("Divide without problem: " + divideTwoNum(42, 2));
System.out.println("Here the problem: " + divideTwoNum(42, 0));
}
static int divideTwoNum(int a, int b) {
int result;
try {
result = a / b; // возвращаем результат деления
} catch (ArithmeticException e) {
System.out.println("Divide by zero is not correct: " + e);
result = -100500; // минус сто-пятьсот
}
return result;
}
}
Теперь всё работает как надо.
Что изменилось? Кусок кода, который может вызвать исключение, «обернули» в конструкцию try-catch (попытаться, и, если что пошло не так, то «схватить»).
Как это работает?
Если в блоке try возникает исключение, указанное в блоке catch, то исполнение блока try прервётся и программа перейдёт к блоку catch.
Т.е. исключение будет «схвачено» / «перехвачено».
В блоке catch («схватить») в скобках обязательно требуется указать, что именно перехватывается и идентификатор:
а в идентификаторе будет отражено сообщение, которое можно также вывести на экран (что и было сделано).
Теперь поработаем с массивом:
"Перехват" некорректного индексаpublic class Program { // Ваш класс может называться по-другому
static int array [] = {1, 2, 3, 4, 5}; // cтатическое поле класса Programm
public static void main(String args[]) {
System.out.println("Array index without problem: " + arrayIndex(2)); // выведет 3
System.out.println("Here the problem with index: " + arrayIndex(5)); // вне диапазона
}
static int arrayIndex(int index) { // возвращает значение поля по индексу
int result;
try {
result = array[index]; // пробуем получить значение по индексу
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Index is not correct: " + e);
result = -100500; // всё те же сто-пятьсот со знаком минус
}
return result;
}
}
Некорректный индекс "перехвачен".
Пример неудачной попытки преобразования:
Ошибка преобразованияpublic class Program { // Ваш класс может называться по-другому
public static void main(String args[]) {
System.out.println("Conversion without problem: " + stringToInt("22")); // выведет 3
System.out.println("Here the problem with conversion: " + stringToInt("two")); // не преобразуется
}
static int stringToInt(String str) {
int result;
try {
result = Integer.parseInt(str); // строку в число (попытка)
} catch (NumberFormatException e) {
System.out.println("Conversion is impossible: " + e);
result = -100500; // всё те же сто-пятьсот со знаком минус
}
return result;
}
}
Проблема "перехвачена".
Обработка нескольких исключений:
Ошибка преобразования и индексаimport java.util.Arrays;
public class Program { // Ваш класс может называться по-другому
public static void main(String args[]) {
int[] numbers = new int[3]; // массив из трёх чисел
try {
numbers[0] = 5;
numbers[1] = 10;
numbers[2] = Integer.parseInt("one"); // не преобразуется
numbers[4] = 45; // такого индекса нет
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Out of range:" + e);
} catch (NumberFormatException e) {
System.out.println("Incorrect conversion: " + e);
}
System.out.println("Array: " + Arrays.toString(numbers));
}
}
Проблема "перехвачена".
В предудущем примере обработается (перехватится) либо одно исключение, либо другое. Об этом мы позаботились заранее. Если же будут иметь место два исключения сразу, то обработается только первое, обработка второго уже видна не будет, но программа продолжит работу.
После завершения выполнения блока catch программа продолжает свою работу, выполняя все остальные инструкции, следующие за catch. Конструкция try..catch также может иметь блок finally.
Однако этот блок необязательный, и его можно при обработке исключений опускать.
Блок finally выполняется в любом случае, возникали ли исключения в блоке try, или нет:
Использование блока "finally"import java.util.Arrays;
public class Program { // Ваш класс может называться по-другому
public static void main(String args[]) {
int[] numbers = new int[2]; // индексы только 0 и 1
try {
numbers[0] = 5;
numbers[1] = 10;
numbers[2] = 45; // нет такого индекса
System.out.println(numbers[4]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Problems with array index: " + e);
} finally {
System.out.print("Finally block's printing anyway: ");
System.out.println("Array: " + Arrays.toString(numbers));
}
}
}
Разработчик может предусмотреть возникновение исключений, даже если не знает, какие именно из них произойдёт:
Получение информации об исключенииpublic class Program { // Ваш класс может называться по-другому
public static void main(String args[]) {
int a = 10, b = 0; // если b не равно нулю, то исключения не будет
try { // но мы не обрабатываем ArithmeticException
System.out.println("a divide b = " + (a / b));
} catch (Exception e) { // просто исключение, какое, заранее не знаем
System.out.println("Exception is: " + e); // теперь знаем какое именно
}
}
}
Информация об исключении получена
Уже упоминалось, что исключения бывают и намеренными. Такие исключения разработчик сам вызывает (бросает, выбрасывает) принудительно.
Почему «бросает»? Потому что используется оператор throw.
То есть с помощью этого оператора мы сами можем создать исключение и вызвать его в процессе выполнения.
Например, в нашей программе происходит ввод возраста, и мы хотим, чтобы в случае, если возраст был введён отрицательным, то возникало исключение.
Тут возможно два способа:
- «Бросить» просто исключение (Exception);
- «Бросить» любое встроенное исключение (например, IllegalArgumentException).
Способ 1:
"Бросаем" исключение (первый приём)public class Program {
public static void main(String[] args) {
int age; //заранее инициализируем неверно
System.out.print("Enter Your age here: ");
try {
Scanner in = new Scanner(System.in);
age = in.nextInt();
if (age <= 0) {
throw new Exception("age cannot be negative or zero.");
}
System.out.println("Your age is: " + age);
} catch (Exception e) {
System.out.println("Problem is: " + e.getMessage());
}
}
}
Отправленное сообщение было прочитано
Способ 2:
"Бросаем" исключение (второй приём)import java.util.Scanner;
import java.util.InputMismatchException;
public class Program {
public static void main(String[] args) {
int age;
System.out.print("Enter Your age here: ");
try {
Scanner in = new Scanner(System.in);
age = in.nextInt();
if (age <= 0) {
throw new IllegalArgumentException(
"age cannot be negative or zero.");
}
System.out.println("Your age is: " + age);
} catch (IllegalArgumentException e) {
System.out.println("Problem is: " + e.getMessage());
}
catch (InputMismatchException e) {
System.out.println("You have to input positive digit value!");
}
}
}
Отправленное сообщение также было прочитано
Подводя итог, хочется отметить:
- Программный код пишется с учетом того, что в любом его месте возможно исключение. Следовательно, необходимо уметь эффективно использовать соответствующие приёмы обработки исключительных ситуаций.
- Обработка исключений, таким образом, позволяет предотвратить аварийное завершение программы и гарантирует её дальнейшую корректную работу в рамках основного алгоритма;
- Конструкция обработки исключений использует ключевые слова: try / catch / finally;
- Есть возможность «провоцировать» определённые ситуации для управления программой, путем «бросания» (throw) исключений. Например, для проверки корректного ввода значений пользователем.