Обработка исключительных ситуаций
Исключительные ситуации (exceptions) могут возникнуть во время выполнения (runtime) программы, прервав ее обычный ход. К ним относится деление на нуль, отсутствие загружаемого файла, отрицательный или вышедший за верхний предел индекс массива, переполнение выделенной памяти и масса других неприятностей, которые могут случиться в самый неподходящий момент.
Конечно, можно предусмотреть такие ситуации и застраховаться от них как-нибудь так:
if (something == wrong){
// Предпринимаем аварийные действия }else{
// Обымный ход действий
}
Но при этом много времени уходит на проверки, и программа превращается в набор этих проверок. Посмотрите любую штатную производственную программу, написанную на языке С или Pascal, и вы увидите, что она на 2/3 состоит из таких проверок.
Кроме того, действия, направленные на выполнение задачи, смешиваются с действиями по обработке исключительных ситуаций. Это затрудняет отладку программы и приводит к скрытым ошибкам, которые трудно обнаружить и устранить.
В объектно-ориентированных языках программирования принят другой подход. При возникновении исключительной ситуации исполняющая система создает объект определенного класса, соответствующего возникшей ситуации. Этот объект содержит сведения о том, что, где и когда произошло. Он передается на обработку программе, в которой возникло исключение. Если программа не обрабатывает исключение, то объект возвращается обработчику исполняющей системы. Этот обработчик поступает очень просто: выводит на консоль сообщение о произошедшем исключении и прекращает выполнение программы.
Приведем пример. В программе листинга 21.1 может возникнуть деление на нуль, если запустить ее с аргументом 0. В программе нет никаких средств обработки такой исключительной ситуации. Посмотрите на рис. 21.1, какие сообщения выводит исполняющая система Java.
Листинг 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.
После первого запуска, при обычном ходе программы, выводятся все сообщения.
После второго запуска, приводящего к делению на нуль, управление сразу же передается в соответствующий блок 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 и указанием класса исключения: