Исключительные ситуации (exceptions) могут возникнуть во время выполнения (runtime) программы, прервав ее обычный ход. К ним относится деление на нуль, отсутствие загружаемого файла, отрицательный или вышедший за верхний предел индекс массива, переполнение выделенной памяти и масса других неприятностей, которые могут случиться в самый неподходящий момент.
Конечно, можно предусмотреть такие ситуации и застраховаться от них как-нибудь так:
if (something == wrong){
// Предпринимаем аварийные действия
}else{
// Обычный ход действий
}
Но при этом много времени уходит на проверки, и программа превращается в набор этих проверок. Посмотрите любую штатную производственную программу, написанную на языке С или Pascal, и увидите, что она на 2/3 состоит из таких проверок.
В объектно-ориентированных языках программирования принят другой подход. При возникновении исключительной ситуации исполняющая система создает объект определенного класса, соответствующего возникшей ситуации, содержащий сведения о том, что, где и когда произошло. Этот объект передается на обработку программе, в которой возникло исключение. Если программа не обрабатывает исключение, то объект возвращается обработчику по умолчанию исполняющей системы. Обработчик поступает очень просто: выводит на консоль сообщение о произошедшем исключении и прекращает выполнение программы.
Приведем пример. В программе листинга 16.1 может возникнуть деление на нуль, если запустить ее с аргументом 0. В программе нет никаких средств обработки такой исключительной ситуации. Посмотрите на рис. 16.1, какие сообщения выводит исполняющая система Java.
Листинг 16.1. Программа без обработки исключений
class SimpleExt {
public static void main(String[] args)({
int n = Integer.parselnt(args[0]);
System.out.println("10 / n = " + (10 / n));
System.out.println("After all actions");
}
}
Рис. 16.1. Сообщения об исключительных ситуациях
Программа SimpleExt запущена три раза. Первый раз аргумент args[0] равен 5 и программа выводит результат: "ю / n = 2". После этого появляется второе сообщение: "After all actions".
Второй раз аргумент равен о, и вместо результата мы получаем сообщение о том, что в подпроцессе "main" произошло исключение класса ArithmeticException вследствие деления на нуль: "/ by zero". Далее уточняется, что исключение возникло при выполнении метода main класса SimpleExt, а в скобках указано, что действие, в результате которого возникла исключительная ситуация, записано в четвертой строке файла SimpleExt.java. Выполнение программы прекращается, заключительное сообщение не появляется.
Третий раз программа запущена вообще без аргумента. В массиве argsn нет элементов, его длина равна нулю, а мы пытаемся обратиться к элементу args [0]. Возникает исключительная ситуация класса ArrayindexOutofBoundsException вследствие действия, записанного в третьей строке файла SimpleExt.java. Выполнение программы прекращается, обращение к методу printino не происходит.
Мы можем перехватить и обработать исключение в программе. При описании обработки применяется бейсбольная терминология. Говорят, что исполняющая система или программа "выбрасывает" (throws) объект-исключение. Этот объект "пролетает" через всю программу, появившись сначала в том методе, где произошло исключение, а программа в одном или нескольких местах пытается (try) его "перехватить" (catch) и обработать. Обработку можно сделать полностью в одном месте, а можно обработать исключение в одном месте, выбросить снова, перехватить в другом месте и обрабатывать дальше.
Мы уже много раз в этой книге сталкивались с необходимостью обрабатывать различные исключительные ситуации, но не делали этого, потому что не хотели отвлекаться от основных конструкций языка. Не вводите это в привычку! Хорошо написанные объектно-ориентированные программы обязательно должны обрабатывать все возникающие в них исключительные ситуации.
Для того чтобы попытаться (try) перехватить (catch) объект-исключение, надо весь код программы, в котором может возникнуть исключительная ситуация, охватить оператором try{} catch о {}. Каждый блок catchou перехватывает исключение только одного типа, того, который указан в его аргументе. Но можно написать несколько блоков catch(){} для перехвата нескольких типов исключений.
Например, мы знаем, что в программе листинга 16.1 могут возникнуть исключения двух типов. Напишем блоки их обработки, как это сделано в листинге 16.2.
Листинг 16.2. Программа с блоками обработки исключений
class SimpleExtlf
public static void main(String[] args){
try{
int n = Integer.parselnt(args[0]);
System.out.println("After parselnt());
System.out.println(" 10 / n = " + (10 / n) ) ;
Systfem. out. println ("After results output");
}catch(ArithmeticException ae){
System.out.println("From Arithm.Exc. catch: "+ae);
}catch(ArraylndexOutOfBoundsException arre){
System.out.println("From Array.Exc.catch: "+arre);
}finally{
System.out.println("From finally");
}
System.out.println("After all actions");
}
}
В программу листинга 16.1 вставлен блок try{} и два блока перехвата catchou для каждого типа исключений. Обработка исключения здесь заключается просто в выводе сообщения и содержимого объекта-исключения, как оно представлено методом tostring() соответствующего класса-исключения.
После блоков перехвата вставлен еще один, необязательный блок finaliy(). Он предназначен для выполнения действий, которые надо выполнить обязательно, чтобы ни случилось. Все, что написано в этом блоке, будет выполнено и при возникновении исключения, и при обычном ходе программы, и даже если выход из блока try{} осуществляется оператором return.
Если в операторе обработки исключений есть блок finally{}, то блок catch () {} может отсутствовать, т. е. можно не перехватывать исключение, но при его возникновении все-таки проделать какие-то обязательные действия.
Кроме блоков перехвата в листинге 16.2 после каждого действия делается трассировочная печать, чтобы можно было проследить за порядком выполнения программы. Программа запущена три раза: с аргументом 5, с аргументом 0 и вообще без аргумента. Результат показан на рис. 16.2.
Рис. 16.2. Сообщения обработки исключений
После первого запуска, при обычном ходе программы, выводятся все сообщения.
После второго запуска, приводящего к делению на нуль, управление сразу
Же передается В соответствующий блок catch(ArithmeticException ае) {}, потом выполняется то, что написано в блоке finally{}.
После третьего запуска управление после выполнения метода parseinto передается В другой блок catch(ArraylndexOutOfBoundsException arre){}, затем в блок finally{}.
Обратите внимание, что во всех случаях — и при обычном ходе программы, и после этих обработок — выводится сообщение "After all actions". Это свидетельствует о том, что выполнение программы не прекращается при возникновении исключительной ситуации, как это было в программе листинга 16.1, а продолжается после обработки и выполнения блока finally*}.
При записи блоков обработки исключений надо совершенно четко представлять себе, как будет передаваться управление во всех случаях. Поэтому изучите внимательно рис. 16.2.
Интересно, что пустой блок catch (){}, в котором между фигурными скобками нет ничего, даже пробела, тоже считается обработкой исключения и приводит к тому, что выполнение программы не прекратится. Именно так мы "обрабатывали" исключения в предыдущих главах.
Немного выше было сказано, что выброшенное исключение "пролетает" через всю программу. Что это означает? Изменим программу листинга 16.2, вынеся деление в отдельный метод f (). Получим листинг 16.3.
Листинг 16.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.parselnt(args[0]);
System.out.println("After parselnt());
f (n);
System.out.println("After results output");
}catch(SrithmeticException ae){
System.out.println("From Arithm.Exc. catch: "+ae);
}catch(ArraylndexQutOfBoundsException arre){
System.out.println("From Array.Exc. catch: "+arre);
}finally{
System,out.println("From finally");
}
System.out.println("After all actions");
}
}
Откомпилировав и запустив программу листинга 16.3, убедимся, что вывод программы не изменился, он такой же, как на рис. 16.2. Исключение, возникшее при делении на нуль в методе f (), "пролетело" через этот метод, "вылетело" в метод main (), там перехвачено и обработано.