Глава 10 - Обработка исключений

В этой главе обсуждается используемый в Java механизм обработки исключений. Исключение в Java - это объект, который описывает исключительное состояние, воз-никшее в каком-либо участке программного кода. Когда возникает ис-ключительное состояние, создается объект класса Exception. Этот объект пересылается в метод, обрабатывающий данный тип исключительной ситуации. Исключения могут возбуждаться и <вруч-ную> для того, чтобы сообщить о некоторых нештатных ситуациях.

Основы

К механизму обработки исключений в Java имеют отношение 5 клю-чевых слов: - try, catch, throw, throws и finally. Схема работы этого механизма следующая. Вы пытаетесь (try) выполнить блок кода, и если при этом возникает ошибка, система возбуждает (throw) исключение, ко-торое в зависимости от его типа вы можете перехватить (catch) или пере-дать умалчиваемому (finally) обработчику.

Ниже приведена общая форма блока обработки исключений.

	try { 
		// блок кода } 
	catch (ТипИсключения1 е) { 
		// обработчик исключений типа ТипИсключения1 } 
	catch (ТипИсключения2 е) { 
		// обработчик исключений типа ТипИсключения2 
	throw(e)   // повторное возбуждение исключения } 
	finally { 
	} 

Замечание

В языке Delphi вместо ключевого слова catch используется except.

Типы исключений

В вершине иерархии исключений стоит класс Throwable. Каждый из типов исключений является подклассом класса Throwable. Два непосредственных наследника класса Throwable делят иерархию подклассов исключений на две различные ветви. Один из них - класс Ехception - используется для описания исключительных ситуации, кото-рые должны перехватываться программным кодом пользователя. Другая ветвь дерева подклассов Throwable - класс Error, который предназначен для описания исклю-чительных ситуаций, которые при обычных условиях не должны перехватываться в пользовательской программе.

Неперехваченные исключения

Объекты-исключения автоматически создаются исполняющей средой Java в результате возникновения определенных исключительных состо-яний. Например, очередная наша программа содержит выражение, при вычислении которого возникает деление на нуль.

class Exc0 { 
	public static void main(string args[]) { 
		int d = 0; 
		int a = 42 / d; 
		}
	} 

Вот вывод, полученный при запуске нашего примера.

С:\> java Exc0 

	java.lang.ArithmeticException: / by zero 
	at Exc0.main(Exc0.java:4) 

Обратите внимание на тот факт что типом возбужденного исклю-чения был не Exception и не Throwable. Это подкласс класса Exception, а именно: ArithmeticException, поясняющий, какая ошибка возникла при выполнении программы. Вот другая версия того же класса, в кото-рой возникает та же исключительная ситуация, но на этот раз не в про-граммном коде метода main.

class Exc1 { 
	static void subroutine() { 
		int d = 0; 
		int a = 10 / d; 
		} 
	public static void main(String args[]) { 
		Exc1.subroutine(); 
		}
	} 

Вывод этой программы показывает, как обработчик исключений ис-полняющей системы Java выводит содержимое всего стека вызовов.

С:\> java Exc1 
	java.lang.ArithmeticException: / by zero 
	at Exc1.subroutine(Exc1.java:4) 
	at Exc1.main(Exc1.java:7) 

try и catch

Для задания блока программного кода, который требуется защитить от исключений, исполь-зуется ключевое слово try. Сразу же после try-блока помещается блок catch, задающий тип исключения которое вы хотите обрабатывать.

class Exc2 { 
	public static void main(String args[]) { 
		try { 
			int d = 0; 
			int a = 42 / d; 
			} 
		catch (ArithmeticException e) { 
			System.out.println("division by zero"); 
			} 
		}
	} 

Целью большинства хорошо сконструированных catch-разделов долж-на быть обработка возникшей исключительной ситуации и приведение переменных программы в некоторое разумное состояние - такое, чтобы программу можно было продолжить так, будто никакой ошибки и не было (в нашем примере выводится предупреждение - division by zero).

Несколько разделов catch

В некоторых случаях один и тот же блок программного кода может воз-буждать исключения различных типов. Для того, чтобы обрабатывать по-добные ситуации, Java позволяет использовать любое количество catch-разделов для try-блока. Наиболее специализированные классы исключений должны идти первыми, поскольку ни один подкласс не будет достигнут, если поставить его после суперкласса. Следующая про-грамма перехватывает два различных типа исключений, причем за этими двумя специализированными обработчиками следует раздел catch общего назначения, перехватывающий все подклассы класса Throwable.

class MultiCatch { 
	public static void main(String args[]) { 
	try { 
		int a = args.length; 
		System.out.println("a = " + a); 
		int b = 42 / a; 
		int c[] = { 1 }; 
		c[42] = 99; 
		} 
	catch (ArithmeticException e) { 
		System.out.println("div by 0: " + e); 
		} 
	catch(ArrayIndexOutOfBoundsException e) { 
		System.out.println("array index oob: " + e); 
		} 
	}
} 

Этот пример, запущенный без параметров, вызывает возбуждение ис-ключительной ситуации деления на нуль. Если же мы зададим в командной строке один или несколько параметров, тем самым установив а в значение боль-ше нуля, наш пример переживет оператор деления, но в следующем опе-раторе будет возбуждено исключение выхода индекса за границы масси-ва ArrayIndexOutOf Bounds. Ниже приведены результаты работы этой программы, за-пущенной и тем и другим способом.

С:\> java MultiCatch 
	а = 0 
	div by 0: java.lang.ArithmeticException: / by zero 
C:\> java MultiCatch 1 
	a = 1 
	array index oob: java.lang.ArrayIndexOutOfBoundsException: 42 

Вложенные операторы try

Операторы try можно вкладывать друг в друга аналогично тому, как можно создавать вложенные области видимости переменных. Если у оператора try низкого уровня нет раздела catch, соответствующего возбужденному исключению, стек будет развернут на одну ступень выше, и в поисках подходящего обработчика будут прове-рены разделы catch внешнего оператора try. Вот пример, в котором два оператора try вложены друг в друга посредством вызова метода.

class MultiNest { 
	static void procedure() { 
		try { 
			int c[] = { 1 }; 
			c[42] = 99; 
			} 
		catch(ArrayIndexOutOfBoundsException e) { 
			System.out.println("array index oob: " + e); 
			}
		} 
	public static void main(String args[]) { 
		try { 
			int a = args.length(); 
			System.out.println("a = " + a); 
			int b = 42 / a; 
			procedure(); 
			} 
		catch (ArithmeticException e) { 
			System.out.println("div by 0: " + e); 
			} 
		}
	} 

throw

Оператор throw используется для возбуждения исключения <вруч-ную>. Для того, чтобы сделать это, нужно иметь объект подкласса клас-са Throwable, который можно либо получить как параметр оператора catch, либо создать с помощью оператора new. Ниже приведена общая форма оператора throw.

throw ОбъектТипаThrowable;

При достижении этого оператора нормальное выполнение кода немед-ленно прекращается, так что следующий за ним оператор не выполня-ется. Ближайший окружающий блок try проверяется на наличие соот-ветствующего возбужденному исключению обработчика catch. Если такой отыщется, управление передается ему. Если нет, проверяется следующий из вложенных операторов try, и так до тех пор пока либо не будет най-ден подходящий раздел catch, либо обработчик исключений исполняю-щей системы Java не остановит программу, выведя при этом состояние стека вызовов. Ниже приведен пример, в котором сначала создается объект-исключение, затем оператор throw возбуждает исключительную ситуацию, после чего то же исключение возбуждается повторно - на этот раз уже кодом перехватившего его в первый раз раздела catch.

class ThrowDemo { 
	static void demoproc() { 
		try { 
			throw new NullPointerException("demo"); 
			} 
		catch (NullPointerException e) { 
			System.out.println("caught inside demoproc"); 
			throw e; 
			}
		} 
	public static void main(String args[]) { 
		try { 
			demoproc(); 
			} 
		catch(NulPointerException e) { 
			System.out.println("recaught: " + e); 
			} 
		}
	} 

В этом примере обработка исключения проводится в два приема. Метод main создает контекст для исключения и вызывает demoproc. Метод demoproc также устанавливает контекст для обработки исключе-ния, создает новый объект класса NullPointerException и с помощью опе-ратора throw возбуждает это исключение. Исключение перехватывается в следующей строке внутри метода demoproc, причем объект-исключение доступен коду обработчика через параметр e. Код обработчика выводит сообщение о том, что возбуждено исключение, а затем снова возбуждает его с помощью оператора throw, в результате чего оно передается обра-ботчику исключений в методе main. Ниже приведен результат, получен-ный при запуске этого примера.

С:\> java ThrowDemo 
	caught inside demoproc 
	recaught: java.lang.NullPointerException: demo 

throws

Если метод способен возбуждать исключения, которые он сам не об-рабатывает, он должен объявить о таком поведении, чтобы вызывающие методы могли защитить себя от этих исключений. Для задания списка исключений, которые могут возбуждаться методом, используется ключе-вое слово throws. Если метод в явном виде (т.е. с помощью оператора throw) возбуждает исключе-ние соответствующего класса, тип класса исключений должен быть ука-зан в операторе throws в объявлении этого метода. С учетом этого наш прежний синтаксис определения метода должен быть расширен следую-щим образом:

	тип имя_метода(список аргументов) throws список_исключений {} 

Ниже приведен пример программы, в которой метод procedure пыта-ется возбудить исключение, не обеспечивая ни программного кода для его перехвата, ни объявления этого исключения в заголовке метода. Такой программный код не будет оттранслирован.

class ThrowsDemo1 { 
	static void procedure() { 
		System.out.println("inside procedure"); 
		throw new IllegalAccessException("demo"); 
		} 
	public static void main(String args[]) { 
		procedure(); 
		}
} 

Для того, чтобы мы смогли оттранслировать этот пример, нам при-дется сообщить транслятору, что procedure может возбуждать исключе-ния типа IllegalAccessException и в методе main добавить код для обработки этого типа исключений :

class ThrowsDemo { 
	static void procedure() throws IllegalAccessException { 
		System.out.println(" inside procedure"); 
		throw new IllegalAccessException("demo"); 
		} 
	public static void main(String args[]) { 
		try { 
			procedure(); 
			} 
		catch (IllegalAccessException e) { 
			System.out.println("caught " + e); 
			} 
		}
	} 

Ниже приведен результат выполнения этой программы.

С:\> java ThrowsDemo 

	inside procedure 
	caught java.lang.IllegalAccessException: demo 

finally

Иногда требуется гарантировать, что определенный участок кода будет выпол-няться независимо от того, какие исключения были возбуждены и пере-хвачены. Для создания такого участка кода используется ключевое слово finally. Даже в тех случаях, когда в методе нет соответствующего воз-бужденному исключению раздела catch, блок finally будет выполнен до того, как управление перейдет к операторам, следующим за разделом try. У каждого раздела try должен быть по крайней мере или один раз-дел catch или блок finally. Блок finally очень удобен для закрытия файлов и освобождения любых других ресурсов, захваченных для времен-ного использования в начале выполнения метода. Ниже приведен пример класса с двумя методами, завершение которых происходит по разным причинам, но в обоих перед выходом выполняется код раздела finally.

class FinallyDemo { 
	static void procA() { 
		try { 
			System.out.println("inside procA"); 
			throw new RuntimeException("demo"); 
			} 
		finally { 
			System.out.println("procA's finally"); 
			} 
		}

	static void procB() { 
		try { 
			System.out.println("inside procB"); 
			return; 
			} 
		finally { 
			System.out.println("procB's finally"); 
			}
		} 

	public static void main(String args[]) { 
		try { 
			procA(); 
			} 
		catch (Exception e) {} 
			procB(); 
			}
		} 

В этом примере в методе procA из-за возбуждения исключения про-исходит преждевременный выход из блока try, но по пути <наружу> вы-полняется раздел finally. Другой метод procB завершает работу выпол-нением стоящего в try-блоке оператора return, но и при этом перед выходом из метода выполняется программный код блока finally. Ниже приведен результат, полученный при выполнении этой программы.

С:\> java FinallyDemo 
	inside procA 
	procA's finally 
	inside procB 
	procB's finally 

Подклассы Exception

Только подклассы класса Throwable могут быть возбуждены или пере-хвачены. Простые типы - int, char и т.п., а также классы, не являю-щиеся подклассами Throwable, например, String и Object, использоваться в качестве исключений не могут. Наиболее общий путь для использова-ния исключений - создание своих собственных подклассов класса Ex-ception. Ниже приведена программа, в которой объявлен новый подкласс класса Exception.

	class MyException extends Exception { 
		private int detail; 
			MyException(int a) { 
			detail = a: 
			} 
		public String toString() { 
			return "MyException["+detail+"]"; 
			} 
		} 

	class ExceptionDemo { 
		static void compute(int a) throws MyException { 
			System.out.println("called computer+a+")."); 
			if (a > 10) throw new MyException(a); 
			System.out.println("normal exit."); 
			} 

		public static void main(String args[]) { 
			try { 
				compute(1); 
				compute(20); 
				} 
			catch (MyException e) { 
				System.out.println("caught" + e); 
				} 
			}
		} 

Этот пример довольно сложен. В нем сделано объявление подкласса MyException класса Exception. У этого подкласса есть специальный кон-структор, который записывает в переменную объекта целочисленное значение, и совмещенный метод toString, выводящий значение, хранящееся в объекте-исключении. Класс ExceptionDemo определяет метод compute, который возбуждает исключение типа MyExcepton. Простая логика метода compute возбуждает исключение в том случае, когда значение пара-ветра метода больше 10. Метод main в защищенном блоке вызывает метод compute сначала с допустимым значением, а затем - с недопус-тимым (больше 10), что позволяет продемонстрировать работу при обоих путях выполнения кода. Ниже приведен результат выполнения програм-мы.

С:\> java ExceptionDemo 
	called compute(1). 
	normal exit. 
	called compute(20). 
	caught MyException[20] 

Заключительное резюме

Обработка исключений предоставляет исключительно мощный меха-низм для управления сложными программами. Try, throw, catch дают вам простой и ясный путь для встраивания обработки ошибок и прочих нештатных ситуаций в программную логи-ку. Если вы научитесь должным об-разом использовать рассмотренные в данной главе механизмы, это при-даст вашим классам профессиональный вид, и любые будущие пользователи вашего программного кода, несомненно, оценят это.