Рсключительные ситуации (exceptions) РјРѕРіСѓС‚ возникнуть РІРѕ время выполнения (runtime) программы, прервав ее обычный С…РѕРґ. Рљ РЅРёРј относится деление РЅР° нуль, отсутствие загружаемого файла, отрицательный или вышедший Р·Р° верхний предел индекс массива, переполнение выделенной памяти Рё масса РґСЂСѓРіРёС… неприятностей, которые РјРѕРіСѓС‚ случиться РІ самый неподходящий момент.
Конечно, можно предусмотреть такие ситуации и застраховаться от них как-нибудь так:
if (something == wrong){
// Предпринимаем аварийные действия }else{
// Обымный ход действий
}
Но при этом много времени уходит на проверки, и программа превращается в набор этих проверок. Посмотрите любую штатную производственную программу, написанную на языке С или Pascal, и вы увидите, что она на 2/3 состоит из таких проверок.
РљСЂРѕРјРµ того, действия, направленные РЅР° выполнение задачи, смешиваются СЃ действиями РїРѕ обработке исключительных ситуаций. Рто затрудняет отладку программы Рё РїСЂРёРІРѕРґРёС‚ Рє скрытым ошибкам, которые трудно обнаружить Рё устранить.
Р’ объектно-ориентированных языках программирования РїСЂРёРЅСЏС‚ РґСЂСѓРіРѕР№ РїРѕРґС…РѕРґ. РџСЂРё возникновении исключительной ситуации исполняющая система создает объект определенного класса, соответствующего возникшей ситуации. Ртот объект содержит сведения Рѕ том, что, РіРґРµ Рё РєРѕРіРґР° произошло. РћРЅ передается РЅР° обработку программе, РІ которой возникло исключение. Если программа РЅРµ обрабатывает исключение, то объект возвращается обработчику исполняющей системы. Ртот обработчик поступает очень просто: выводит РЅР° консоль сообщение Рѕ произошедшем исключении Рё прекращает выполнение программы.
Приведем пример. В программе листинга 21.1 может возникнуть деление на нуль, если запустить ее с аргументом 0. В программе нет никаких средств обработки такой исключительной ситуации. Посмотрите на рис. 21.1, какие сообщения выводит исполняющая система Java.
Рис. 21.1. Сообщения об исключительных ситуациях |
Листинг 21.1. Программа без обработки исключений
class SimpleExt{
public static void main(String[] args){ int n = Integer.parseInt(args[0]);
System.out.println("10 / n = " + (10 / n));
System.out.println("After all actions");
}
}
Программа SimpleExt запущена три раза. Первый раз аргумент args[0] равен 5 и программа выводит результат: "10 / n = 2". После этого появляется второе сообщение:
"After all actions".
Второй раз аргумент равен 0, и вместо результата мы получаем сообщение о том, что в подпроцессе "main" произошло исключение класса ArithmeticException вследствие деления на нуль: "/ by zero". Далее уточняется, что исключение возникло при выполнении метода main класса SimpleExt, а в скобках указано, что действие, в результате которого возникла исключительная ситуация, записано в четвертой строке файла SimpleExtjava. Выполнение программы на этом прекращается, заключительное сообщение не появляется.
Третий раз программа запущена вообще без аргумента. В массиве args [ ] нет элементов, его длина равна нулю, а мы пытаемся обратиться к элементу args[0]. Возникает исключительная ситуация класса ArrayIndexOutOfBoundsException вследствие действия, записанного в третьей строке файла SimpleExtjava. Выполнение программы прекращается, обращение к методу println() не происходит.
Блоки перехвата исключения
РњС‹ можем перехватить Рё обработать исключение РІ программе. РџСЂРё описании обработки применяется бейсбольная терминология. Говорят, что исполняющая система или программа "выбрасывает" (throws) объект-исключение. Ртот объект "пролетает" через РІСЃСЋ программу, появившись сначала РІ том методе, РіРґРµ произошло исключение. Программа РІ РѕРґРЅРѕРј или нескольких местах пытается (try) его "перехватить" (catch) Рё обработать. Обработку можно сделать полностью РІ РѕРґРЅРѕРј месте, Р° можно частично обработать исключение РІ РѕРґРЅРѕРј месте, выбросить СЃРЅРѕРІР°, перехватить РІ РґСЂСѓРіРѕРј месте Рё обрабатывать дальше.
Мы уже много раз в этой книге сталкивались с необходимостью обрабатывать различные исключительные ситуации, но не делали этого, потому что не хотели отвлекаться от основных конструкций языка. Не вводите это в привычку! Хорошо написанные объектно-ориентированные программы обязательно должны обрабатывать все возникающие в них исключительные ситуации.
Для того чтобы попытаться (try) перехватить (catch) объект-исключение, надо весь код программы, в котором может возникнуть исключительная ситуация, охватить оператором try{} catch () {}. Каждый блок catch(){} перехватывает исключение одного или нескольких типов — они указываются в его параметре. Можно написать несколько блоков catch (){} для перехвата нескольких типов исключений.
Например, мы знаем, что в программе листинга 21.1 могут возникнуть исключения двух типов. Напишем блоки их обработки, как это сделано в листинге 21.2.
Листинг 21.2. Программа с блоками обработки исключений
class SimpleExt1{
public static void main(String[] args){ try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
System.out.println(" 10 / n = " + (10 / n));
System.out.println("After results output");
}catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: " + ae);
}catch(ArrayIndexOutOfBoundsException arre){
System.out.println("From Array.Exc. catch: " + arre);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
В программу листинга 21.2 вставлен блок try{} и два блока перехвата catch(){} для каждого типа исключений. Обработка исключения здесь заключается просто в выводе сообщения и содержимого объекта-исключения, как оно представлено методом toString () соответствующего класса-исключения.
После блоков перехвата вставлен еще один, необязательный блок finally{}. Он предназначен для выполнения действий, которые надо выполнить обязательно, что бы ни случилось. Все, что написано в этом блоке, будет выполнено и при возникновении исключения, и при обычном ходе программы, и даже если выход из блока try{} или из блока catch (){} осуществляется оператором return. В последнем случае оператор return выполняется после блока finally{}.
Если в операторе обработки исключений есть блок finally{}, то блок catch() {} может отсутствовать, т. е. можно не перехватывать исключение, но при его возникновении все-таки проделать какие-то обязательные действия.
Кроме блоков перехвата в листинге 21.2 после каждого действия выполняется трассировочная печать, чтобы можно было проследить за порядком выполнения программы. Программа запущена три раза: с аргументом 5, с аргументом 0 и вообще без аргумента. Результат показан на рис. 21.2.
Рис. 21.2. Сообщения обработки исключений |
После первого запуска, при обычном ходе программы, выводятся все сообщения.
После второго запуска, приводящего к делению на нуль, управление сразу же передается в соответствующий блок catch(ArithmeticException ae) {}, потом выполняется то, что написано в блоке finally{}.
После третьего запуска управление после выполнения метода parseInt () передается в другой блок catch(ArrayIndexOutOfBoundsException arre) {}, затем в блок finally{}.
Обратите внимание, что РІРѕ всех случаях — Рё РїСЂРё обычном С…РѕРґРµ программы, Рё после этих обработок — выводится сообщение "After all actions". Рто свидетельствует Рѕ том, что выполнение программы РЅРµ прекращается РїСЂРё возникновении исключительной ситуации, как это было РІ программе листинга 21.1, Р° продолжается после обработки Рё выполнения блока finally{}.
При записи блоков обработки исключений надо совершенно четко представлять себе, как будет передаваться управление во всех случаях. Поэтому изучите внимательно рис. 21.2.
Рнтересно, что пустой блок catch() {}, РІ котором между фигурными скобками нет ничего, даже пробела, тоже считается обработкой исключения Рё РїСЂРёРІРѕРґРёС‚ Рє тому, что выполнение программы РЅРµ прекратится. Рменно так РјС‹ "обрабатывали" исключения РІ предыдущих главах.
Немного ранее было сказано, что выброшенное исключение "пролетает" через РІСЃСЋ программу. Что это означает? Рзменим программу листинга 21.2, вынеся деление РІ отдельный метод f(). Получим листинг 21.3.
Листинг 21.3. Выбрасывание исключения из метода
class SimpleExt2{
private static void f(int n){
System.out.println(" 10 / n = " + (10 / n));
}
public static void main(String[] args){ try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()"); f(n);
System.out.println("After results output");
}catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: " + ae);
}catch(ArrayIndexOutOfBoundsException arre){
System.out.println("From Array.Exc. catch: " + arre);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
Скомпилировав Рё запустив программу листинга 21.3, убедимся, что вывод программы РЅРµ изменился, РѕРЅ такой же, как РЅР° СЂРёСЃ. 21.2. Рсключение, возникшее РїСЂРё делении РЅР° нуль РІ методе f(), "пролетело" через этот метод, "вылетело" РІ метод main(), там перехвачено Рё обработано.
Упражнения
1. Просмотрите внимательно листинги предыдущих глав и подумайте, где в них требуется обработка исключительных ситуаций.
2. Вставьте в листинги предыдущих глав обработку исключительных ситуаций.
Часть заголовка метода throws
То обстоятельство, что метод не обрабатывает возникающее в нем исключение, а выбрасывает (throws) его, следует отмечать в заголовке метода служебным словом throws и указанием класса исключения:
private static void f(int n) throws ArithmeticException{
System.out.println(" 10 / n = " + (10 / n));
}
Почему же РјС‹ РЅРµ сделали это РІ листинге 21.3? Дело РІ том, что спецификация JLS делит РІСЃРµ исключения РЅР° проверяемые (checked), те, которые проверяет компилятор, Рё непроверяемые (unchecked). РџСЂРё проверке компилятор замечает необработанные РІ методах Рё конструкторах исключения Рё считает ошибкой отсутствие РІ заголовке таких методов Рё конструкторов пометки throws. Рменно для предотвращения подобных ошибок РјС‹ РІ предыдущих главах вставляли РІ листинги блоки обработки исключений.
Так вот, исключения класса RuntimeException и его подклассов, одним из которых является ArithmeticException, — непроверяемые, для них пометка throws необязательна. Еще одно большое семейство непроверяемых исключений составляет класс Error и его расширения.
Почему компилятор не проверяет эти типы исключений? Причина в том, что исключения класса RuntimeException свидетельствуют об ошибках в программе, и единственно разумный метод их обработки — исправить исходный текст программы и перекомпилировать ее. Что касается класса Error, то эти исключения очень трудно локализовать и на стадии компиляции невозможно определить место их появления.
Напротив, возникновение проверяемого исключения показывает, что программа недостаточно продумана, не все возможные ситуации описаны. Такая программа должна быть доработана, о чем и напоминает компилятор.
Если метод или конструктор выбрасывает несколько исключений, то их надо перечислить через запятую после слова throws. Заголовок метода main() листинга 21.1, если бы исключения, которые он выбрасывает, не были бы объектами подклассов класса RuntimeException, следовало бы написать так:
public static void main(String[] args)
throws ArithmeticException, ArrayIndexOutOfBoundsException{
// Содержимое метода
}
Перенесем теперь обработку деления на нуль в метод f() и добавим трассировочную печать, как это сделано в листинге 21.4. Результат показан на рис. 21.3.
Листинг 21.4. Обработка исключения в методе
class SimpleExt3{
private static void f(int n){ // throws ArithmeticException{ try{
System.out.println(" 10 / n = " + (10 / n)); System.out.println("From f() after results output"); }catch(ArithmeticException ae){
System.out.println("From f() catch: " + ae);
// throw ae;
}finally{
System.out.println("From f() finally");
}
public static void main(String[] args){ try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()"); f(n);
System.out.println("After results output"); }catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: " + ae);
}catch(ArrayIndexOutOfBoundsException arre){
System.out.println("From Array.Exc. catch: " + arre); }finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
Внимательно проследите за передачей управления и заметьте, что исключение класса ArithmeticException уже не выбрасывается в метод main ( ).
Оператор try{}catch(){} в методе f() можно рассматривать как вложенный в оператор обработки исключений, написанный в методе main ().
При необходимости исключение можно выбросить оператором throw ae. В листинге 21.4 этот оператор заключен в комментарий. Уберите символы комментария //, перекомпилируйте программу и посмотрите, как изменится ее вывод.
При переопределении в подклассах метода, выбрасывающего исключения, либо вообще не пишется часть заголовка throws, либо снова перечисляются те же классы исключений или их подклассы. Кроме того, можно перечислить не все классы.
Рис. 21.3. Обработка исключения в методе |
Оператор throw
Ртот оператор очень РїСЂРѕСЃС‚: после слова throw через пробел записывается объект класса-исключения. Достаточно часто РѕРЅ создается РїСЂСЏРјРѕ РІ операторе throw, например:
throw new ArithmeticException();
Оператор можно записать в любом месте программы. Он немедленно выбрасывает записанный в нем объект-исключение и дальше обработка этого исключения идет как обычно, будто бы здесь произошло деление на нуль или другое действие, вызвавшее исключение класса ArithmeticException.
Обработка нескольких типов исключений с помощью иерархии
Каждый блок catch () {} перехватывает, как правило, РѕРґРёРЅ определенный тип исключений. Если требуется одинаково обработать несколько типов исключений, то можно перечислить РёС… РІ аргументе блока catch через вертикальную черту (так называемый multicatch) или воспользоваться тем, что классы-исключения образуют иерархию. Применим сначала второй СЃРїРѕСЃРѕР±. Рзменим еще раз листинг 21.2, получив листинг 21.5.
Листинг 21.5. Обработка нескольких типов исключений с помощью иерархии
class SimpleExt4{
public static void main(String[] args){ try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
System.out.println(" 10 / n = " + (10 / n));
System.out.println("After results output");
}catch(RuntimeException ae){
System.out.println("From Run.Exc. catch: " + ae);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
В листинге 21.5 два блока catch(){} заменены одним блоком, перехватывающим исключение класса RuntimeException. Как видно на рис. 21.4, данный блок перехватывает оба исключения. Почему? Потому что это исключения подклассов класса
RuntimeException.
Таким образом, перемещаясь по иерархии классов-исключений, мы можем обрабатывать сразу более или менее крупные совокупности исключений. Рассмотрим подробнее иерархию классов-исключений.
Рис. 21.4. Перехват нескольких типов исключений |
Рерархия классов-исключений
Все классы-исключения расширяют класс Throwable — непосредственное расширение класса Object.
У класса Throwable и у всех его расширений по традиции два конструктора:
□ Throwable () — конструктор по умолчанию;
□ Throwable(String message) — создаваемый объект будет содержать произвольное сообщение message.
Записанное в конструкторе сообщение можно получить затем методом getMessage ( ). Если объект создавался конструктором по умолчанию, то данный метод возвратит null.
Метод toString () возвращает краткое описание события, именно он работал в предыдущих листингах.
Три метода выводят сообщения обо всех методах, встретившихся по пути "полета" исключения:
□ printStackTrace () — выводит сообщения в стандартный вывод, как правило, это консоль;
□ printStackTrace (PrintStream stream) — выводит сообщения в байтовый поток stream;
□ printStackTrace (PrintWriter stream) — выводит сообщения в символьный поток stream.
У класса Throwable два непосредственных наследника — классы Error и Exception. Они не добавляют новых методов, а служат для разделения классов-исключений на два больших семейства — семейство классов-ошибок (error) и семейство собственно классов-исключений (exception).
Классы-ошибки, расширяющие класс Error, свидетельствуют Рѕ возникновении сложных ситуаций РІ виртуальной машине Java. РС… обработка требует глубокого понимания всех тонкостей работы JVM. Ее РЅРµ рекомендуется выполнять РІ обычной программе. РќРµ советуют даже выбрасывать ошибки оператором throw. РќРµ следует делать СЃРІРѕРё классы-исключения расширениями класса Error или какого-то его подкласса.
Рмена классов-ошибок, РїРѕ соглашению, заканчиваются словом Error.
Классы-исключения, расширяющие класс Exception, отмечают возникновение обычной нештатной ситуации, которую можно и даже нужно обработать. Такие исключения следует выбросить оператором throw. Классов-исключений очень много, несколько сотен. Они разбросаны буквально по всем пакетам Java SE API. В большинстве случаев вы можете подобрать готовые классы-исключения для обработки исключительных ситуаций в своей программе. При желании можно создать и свои классы-исключения, расширив класс Exception или любой его подкласс.
Среди классов-исключений выделяется класс RuntimeException — РїСЂСЏРјРѕРµ расширение класса Exception. Р’ нем Рё его подклассах отмечаются исключения, возникшие РїСЂРё работе JVM, РЅРѕ РЅРµ столь серьезные, как ошибки. РС… можно обрабатывать Рё выбрасывать, расширять СЃРІРѕРёРјРё классами, РЅРѕ лучше доверить это JVM, поскольку чаще всего это просто ошибка РІ программе, которую надо исправить. Особенность исключений данного класса РІ том, что РёС… РЅРµ обязательно отмечать РІ заголовке метода словом throws.
Рмена классов-исключений, РїРѕ соглашению, заканчиваются словом Exception.
Порядок обработки исключений
Блоки catch () {} перехватывают исключения в порядке написания этих блоков. Данное правило приводит к интересным результатам.
Р’ листинге 21.2 РјС‹ записали РґРІР° блока перехвата catch(){}, Рё РѕР±Р° блока выполнялись РїСЂРё возникновении соответствующего исключения. Рто происходило потому, что классы-исключения ArithmeticException Рё ArrayIndexOutOfBoundsException находятся РЅР° разных ветвях иерархии исключений. Рначе обстоит дело, если блоки catch(){} перехватывают исключения, расположенные РЅР° РѕРґРЅРѕР№ ветви. Если РІ листинге 21.5 после блока, перехватывающего RuntimeException, поместить блок, обрабатывающий выход индекса Р·Р° пределы:
try{
// Операторы, вызывающие исключения }catch(RuntimeException re){
// Какая-то обработка }catch(ArrayIndexOutOfBoundsException ae){
// Никогда не будет выполнен!
}
то он не будет выполняться, поскольку исключение этого типа является к тому же исключением общего типа RuntimeException и будет перехватываться предыдущим блоком catch (){}. Впрочем, компилятор сообщит вам о том, что исключение типа
ArrayOutOfBoundsException уже перехватывается блоком, обрабатывающим RuntimeException.
Упражнение
3. Обработайте все возможные исключительные ситуации, возникающие в предыдущих лис
тингах.
Обработка нескольких типов исключений с помощью перечисления
В листинге 21.5 обрабатывается исключение типа RuntimeException. К этому типу относятся типы ArithmeticException и ArrayIndexOutOfBoundsException и таким образом достигается их одинаковая обработка. Но к RuntimeException относится еще добрая сотня типов исключений, которые тоже будут обрабатываться в этом блоке catch (){}. Начиная с Java SE 7 добавлена конструкция, названная multi-catch. Если в нашем примере нужно обработать только типы ArithmeticException и ArrayIndexOutOfBoundsException, то можно перечислить их через вертикальную черту в аргументе блока catch (вспомните операцию дизъюнкции). Так сделано в листинге 21.6.
Листинг 21.6. Обработка нескольких типов исключений с помощью перечисления
class SimpleExt5{
public static void main(String[] args){ try{
int n = Integer.parseInt(args[0]);
System.out.println("After parseInt()");
System.out.println(" 10 / n = " + (10 / n));
System.out.println("After results output");
}catch(ArithmeticException | ArrayIndexOutOfBoundsException ae){ System.out.println("From two Exceptions catch: " + ae);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
Перечислять можно, разумеется, не только два, но и больше классов, при этом действует такое же правило иерархии: вместе с перечисленными типами будут обрабатываться все их подтипы.
Создание собственных исключений
Прежде всего, нужно четко определить ситуации, в которых будет возникать ваше собственное исключение, и подумать, не станет ли его перехват невольно причиной перехвата также и других, не учтенных вами исключений.
Потом надо выбрать суперкласс создаваемого класса-исключения. РРј может быть класс Exception или РѕРґРёРЅ РёР· его многочисленных подклассов.
После этого можно написать класс-исключение. Его имя, по соглашению, должно завершаться словом Exception. Как правило, этот класс состоит только из двух конструкторов и переопределения методов toString () и getMessage ().
Рассмотрим простой пример. Пусть метод handle(int cipher) обрабатывает арабские цифры 0—9, которые передаются ему в аргументе cipher типа int. Мы хотим выбросить исключение, если аргумент cipher выходит за диапазон 0—9.
Прежде всего, убедимся, что такого исключения нет в иерархии классов Exception. Ко всему прочему, не отслеживается и более общая ситуация попадания целого числа в какой-то диапазон. Поэтому будем расширять наш класс, который назовем CipherException, прямо от класса Exception. Определим класс CipherException, как показано в листинге 21.7, и используем его в классе ExceptDemo. На рис. 21.5 продемонстрирован вывод этой программы.
Листинг 21.7. Создание класса-исключения
class CipherException extends Exception{ private String msg;
CipherException(){ msg = null; } CipherException(String s){ msg = s; } public String toString(){
return "CipherException (" + msg + ")";
}
} public class ExceptDemo{
static public void handle(int cipher) throws CipherException{ System.out.println("handle()’s beginning"); if (cipher < 0 || cipher > 9)
throw new CipherException("" + cipher); System.out.println("handle()’s ending");
}
public static void main(String[] args){ try{
handle(1); handle(10);
}catch(CipherException ce){
System.out.println("caught " + ce); ce.printStackTrace();
}
}
}
Рис. 21.5. Обработка собственного исключения |
Заключение
Обработка исключительных ситуаций стала сейчас обязательной частью объектноориентированных программ. Применяя методы классов Java SE и других пакетов, обращайте внимание на то, какие исключения они выбрасывают, и обрабатывайте их. При этом помните о том, что исключения резко меняют ход выполнения программы, делают его запутанным. Не увлекайтесь сложной обработкой, помните о принципе KISS.
Например, из блока finally{} можно выбросить исключение и обработать его в другом месте. Подумайте, что произойдет в этом случае с исключением, возникшем в блоке try{}? Оно нигде не будет перехвачено и обработано.
Вопросы для самопроверки
1. Почему в объектно-ориентированных языках принята модель обработки исключительных ситуаций, отличная от модели, принятой в процедурных языках?
2. Можно ли вкладывать друг в друга блоки обработки исключений?
3. Можно ли в блоках обработки исключений применить оператор return?
4. Можно ли в блоках обработки исключений снова вызвать исключение?
5. Можно ли в одном блоке обработки исключений написать несколько блоков catch (){}?
6. Можно ли в одном блоке обработки исключений написать несколько блоков finally{}?
7. В каких случаях в заголовке метода, выбрасывающего исключения, может отсутствовать часть throws?
8. С какой целью создана разветвленная иерархия классов исключений?
9. Можно ли создать свои собственные классы-исключения?
ГЛАВА 22