Обработка исключительных ситуаций
Исключительные ситуации (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 и указанием класса исключения: