Обработка исключений
Исключение – это аварийное состояние, которое возникает при выполнении программы. Для описания и обработки исключительной ситуации в Java создается объект. Исключения могут быть сгенерированы средой исполнения, или прикладной программой. Исключения, сгенерированные вручную, обычно используются, чтобы сообщить вызывающей программе о некоторой аварийной ситуации. Обработка исключений в Java управляется с помощью пяти ключевых слов — try, catch, throw, throws и finally.
Типы исключений
Все типы исключений являются подклассами встроенного класса Throwable. Таким образом, Throwable представляет собой вершину иерархии классов исключений. Непосредственно ниже Throwable находятся два подкласса, которые разделяют исключения на две различные ветви. Одна ветвь возглавляется классом Exception. Этот класс используется для исключительных состояний, которые должны перехватывать программы пользователя. Это также класс, в подклассах которого вы будете создавать ваши собственные заказные типы исключений. У Exception имеется важный подкласс, называемый RuntimeException. Другую ветвь возглавляет класс Error, определяющий исключения, перехват которых вашей программой при нормальных обстоятельствах не ожидается. Исключения типа Error применяются исполнительной системой Java для указания ошибок, имеющих отношение непосредственно к среде времени выполнения. Пример подобной ошибки — переполнение стека.
В спецификации языка Java любая исключительная ситуация, производная от класса Error или RuntimeError, называется "неконтролируемой' (unchecked). Все остальные исключительные ситуации называются контролируемыми (checked).
Что происходит без обработки исключений
Прежде чем узнать, как обрабатывать исключения в своей программе, полезно посмотреть, что произойдет, если их не обрабатывать. Следующая маленькая программа включает выражение, которое преднамеренно вызывает ошибку деления на нуль. (Exc0.java)
class Ехс0 {
public static void main(String args[]) {
int d = 0;
int a = 42 / d;
}
}
Когда исполнительная система Java обнаруживает попытку деления на нуль она создает новый объект исключения и затем активизирует его (вбрасывает в выполняющийся метод). Это заставляет выполнение Ехс0 остановиться, потому что, как только исключение окажется вброшенным, оно должно быть захвачено обработчиком исключений, и причем — немедленно. В представленном примере мы не снабдили набор имеющихся обработчиков исключений своим собственным вариантом, так что исключение захватывается обработчиком, заданным исполнительной системой Java по умолчанию. Любое исключение, которое не захвачено программой, будет в конечном счете выполнено обработчиком по умолчанию. Этот обработчик отображает (на экран) строку, описывающую исключение, печатает трассу стека от точки, в которой произошло исключение, и завершает программу.
Вот какой вывод генерирует предложенный пример, когда он выполняется стандартным Java-интерпретатором из JDK:
Java.lang.AritnmeticException: / by zero
at Ехс0,main(Ехс0.Java:4)
Трасса стека содержит следующие элементы: имя класса (Ехс0), имя метода (main), имя файла (Exe0.java) и номер строки (4), тип исключения ArithmeticException.
Трасса стека всегда показывает последовательность вызовов методов, которые привели к ошибке. Например, вот другая версия предыдущей программы, которая представляет ту же самую ошибку, но в отдельном от main() методе: (Exc1.java)
class Exc1 {
static void subroutine () {
int d = 0;
int a = 10 / d;
}
public static void main(String args[]) {
Exc1. subroutine ();
}
}
Результирующая трасса стека (полученная от обработчика исключений по умолчанию) показывает, как отображается полный стек вызовов:
Java.lang.ArithmeticException: / by zero
at Exc1.subroutine(Exc1.Java:4)
at Exc1.main(Exc1.Java:7)
Как вы видите, основанием стека является строка 7 метода main, которая является обращением к методу subroutine(), генерирующему исключение в строке 4. Стек вызовов весьма полезен для отладки, потому что он довольно точно отражает последовательность шагов, которые привели к ошибке.
Использование операторов try и catch
Самостоятельная обработка исключений обеспечивает два преимущества: во-первых, позволяет фиксировать ошибку и, во-вторых, предохраняет программу от автоматического завершения.
Для того чтобы отслеживать и обрабатывать ошибку времени выполнения, включите код, который нужно контролировать, внутрь блока try. Сразу после блока try укажите catch -блок, определяющий тип исключения, которое нужно перехватить, и его обработчик. Например, так может выглядеть обработка исключения типа ArithmeticException, генерируемое ошибкой "деление на нуль": (Exc2.java)
class Ехс2 {
public static void main(String arcs[]) {
int d, a;
try { // контролировать блок кода
d = 0;
a = 42 / d;
System. out. println("Это не будет напечатано.");
} catch (ArithmeticException e) { // перехватить ошибку
// деления на нуль
System. out. println("Деление на нуль.");
}
System. out. println("После оператора catch.");
}
}
Эта программа генерирует следующий вывод:
Деление на нуль.
После оператора catch.
Обращение к println() внутри блока try никогда не выполняется. Как только исключение возникло, управление программой передается из блока try в блок catch. Управление (выполнением) никогда не возвращается из блока catch блоку try. Таким образом, строка "Это не будет напечатано." никогда не выведется на экран. Сразу после выполнения оператора catch программное управление продолжается со строки, следующей за try/catch.
Таким образом, try и его catch -оператор формируют небольшой программный модуль (точнее — пару связанных блоков). Область видимости catch -утверждения ограничена ближайшим предшествующим утверждением try. Оператор catch не может захватывать исключение, выброшенное другим try -оператором (кроме случая вложенных try -операторов, кратко описанных далее). Операторы, которые контролируются утверждением try, должны быть окружены фигурными скобками (т. е. они должны быть внутри блока). Нельзя использовать try с одиночным утверждением (без скобок).
Хорошо сконструированное catch -предложение обеспечивает фиксацию исключения с последующим продолжением выполнения программы, как будто ошибка никогда не возникала. Например, в следующей программе каждая итерация цикла for получает два случайных целых числа. Они делятся друг на друга, и их частное используется для деления значения 12345. Конечный результат помещается в переменную а. Если любая операция деления приводит к ошибке деления на нуль, она перехватывается, значение сбрасывается в нуль, и программа продолжается. (HandleError.java)
// Обработать исключение и продолжить.
import java.util.Random;
class HandleError {
public static void main(String args[]) {
int a=0, b=0, c=0;
Random r = new Random();
for (int i=0; i<32000; i++) {
try {
b = r.nextInt();
c = r.nextInt();
a = 12345/(b/c);
} catch (ArithmeticException e) {
System. out. println("Деление на нуль.");
a = 0; // сбросить в нуль и продолжить
}
System. out. println("а: " + a);
}
}
}
Множественные операторы catch
В некоторых случаях на одном участке кода может возникнуть более одного исключения. Чтобы обрабатывать этот тип ситуации, необходимо определить несколько операторов catch, каждый — для захвата своего типа исключения. Когда возникает исключение, каждый catch-оператор просматривается по порядку и первый, чей тип соответствует типу возникшего исключения, выполняется. После того как этот catch-оператор выполнится, другие — обходятся, и выполнение продолжается после блока try/catch. Следующий пример отлавливает исключение двух различных типов: (MultiCatch.java)
// Демонстрация множественных catch-операторов.
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("Деление на нуль: " + e);
} catch (ArrayIndexOutOfBoundsException e) {
System. out. println("Индекс элемента массива C:"+e);
}
System. out. println("После блока try/catch.");
}
}
Эта программа выбросит исключение "деление на нуль", если она будет запускаться без параметров командной строки, так как переменная а будет равна нулю. Этой ситуации не возникнет, если вы укажете аргумент в командной строке, устанавливающий в а что-то большее, чем нуль. Но это вызовет исключение ArrayIndexOutOfBoundsException, так как целочисленный массив C имеет длину 1, тогда как программа пытается назначить некоторое значение его сорок второму элементу C[42].
Ниже показаны экранные протоколы, показывающие оба варианта выполнения:
C:\>java MultiCatch
а = 0
Деление на нуль: Java.lang.ArithmeticException: / by zero
После блока try/catch.
C:\>java KultiCatch TestArg
a = 1
Индекс элемента массива oob:Java.lang.ArraylndexOutOfBoundsException:42
После блока try/catch.
Когда вы используете множественные catch -операторы, важно помнить, что в последовательности catch-предложений подклассы исключений должны следовать перед любым из их суперклассов. Это происходит потому, что предложение catch, которое использует суперкласс, будет перехватывать исключения как своего типа, так и любого из своих подклассов. Таким образом, подкласс никогда не был бы достигнут, если бы он следовал после своего суперкласса. Кроме того, в Java недостижимый код — ошибка. Например, рассмотрим следующую программу: (SuperSubCatch.java)
/* Эта программа содержит ошибку. Подкласс должен следовать раньше своего суперкласса в серии catch-операторов. Если это не так, то в результате будет создаваться недостижимый код и соответствующий тип ошибки времени выполнения. */
class SuperSubCatch {
public static void main(String args[]) {
try {
int a = 0;
int b = 42/a;
} catch (Exception e) {
System. out. println("Генерация исключения catch.");
}
/* Этот catch никогда не будет достигнут из-за того, что
ArithmeticException является подклассом Exception. */
catch (ArithmeticException е) {// ОШИБКА. Оператор недостижим
System. out. println("Недостижимый оператор.");
}
}
}
Если вы попытаетесь откомпилировать данную программу, то примете сообщение об ошибке, заявляющее, что второй catch-оператор недостижим. Так как ArithmeticException — подкласс Exception, первый catch-onepaтор обработает все ошибки, основанные на Exception, включая и ArithmeticException. Это означает, что второй catch-оператор никогда не будет выполняться. Чтобы устранить проблему, измените порядок операторов catch.
Вложенные операторы try
Операторы try могут быть вложенными. То есть один try -оператор может находиться внутри блока другого оператора try. При входе в блок try контекст соответствующего исключения помещается в стек. Если внутренний оператор try не имеет catch -обработчика для специфического исключения стек раскручивается, и просматривается следующий catch -обработчик try оператора (для поиска соответствия с типом исключения). Процесс продолжается до тех пор, пока не будет достигнут подходящий catch -оператор или пока все вложенные операторы try не будут исчерпаны. Если согласующегося оператора catch нет, то исключение обработает обработчик исполнительной система Java. Пример, который использует вложенные операторы try:
// Пример вложенных try-операторов.
class NestTry {
public static void main(String args[]) {
try {
int a = args.length;
/* Если нет аргументов командной строки, следующий оператор будет генерировать исключение деления на нуль. */
int b = 42 / a;
System. out. println("а = " + a);
try { // вложенный try-блок
/* Если используется один аргумент командной строки, то следующий код будет генерировать исключение деления на нуль. */
if (a==1) a = a/(a-a); // деление на нуль
/* Если используется два аргумента командной строки, то генерируется исключение выхода за границу массива. */
if (a==2) {
int с[] = { 1 };
с[42]=99; // генерировать исключение
} // выхода за границу массива
} catch (ArrayIndexOutOfBoundsException е) {
System. out. println("Индекс за границей массива:"+е);
}
} catch (ArithmeticException е) {
System. out. println("Деление на нуль: " + е);
}
}
}
Пример демонстрирует вложение одного try -блока в другой. При выполнении программы без аргументов командной строки исключение деления на нуль генерируется внешним блоком try. Выполнение программы с одним аргументом командной строки генерирует исключение деления на нуль внутри вложенного блока try. Так как внутренний блок не захватывает это исключение, он пересылает его внешнему блоку try, где оно и обрабатывается. Если вы запустите программу с двумя аргументами командной строки, то во внутреннем блоке try генерируется исключение нарушения границы массива. Следующие протоколы выполнения иллюстрируют каждый из этих случаев:
С:\>java NestTry
Деление на нуль: Java.lang.ArithmeticException: / by zero
C:\>java NestTry One
a = 1
Деление на нуль: Java.lang.ArithmeticException: / by zero
C:\>java NestTry One Two
a = 2
Индекс выхолит за границу массива:
Java.lang.ArraylndexOutOfBoundsException: 42
Вложение инструкций try может происходить менее очевидными способами, когда вложенный try -блок организован в отдельном методе и выполняется через вызов этого метода во внешнем блоке. Ниже показана предыдущая программа, переписанная так, чтобы вложенный блок try был перемещен внутрь метода nesttry():
/* Try-операторы можно неявно вкладывать через вызовы методов. */
class MethNestTry {
static void nesttry(int a) {
try { // вложенный try-блок
/* Если используется один аргумент командной строки, то следующий код будет генерировать исключение деления на нуль. */
if (a==1) a = a/(a-a); // деление на нуль
/* Если используется два аргумента командной строки,
то генерируется исключение выхода за границу массива. */
if (a==2) {
int с[] = { 1 };
с[42]=99; // генерировать исключение
} // выхода за границу массива
} catch (ArrayIndexOutOfBoundsException е) {
System. out. println("Индекс за гран. массива: " + е);
}
}
public static void main(String args[]) {
try {
int a = args.length;
/* Если нет аргументов командной строки,следующий оператор будет генерироватьисключение деления на нуль. */
int b = 42 / a;
System. out. println ("а = " + a);
nesttry (a);
} catch (ArithmeticException e) {
System. out. println("Деление на нуль: " + e);
}
}
}
Вывод этой программы идентичен выводу предыдущего примера.
Оператор throw
В предыдущих примерах захватывались только исключения, которые вбрасывались исполнительной системой Java. Однако ваша программа может сама явно вбрасывать исключения используя оператор throw. Общая форма оператора throw такова:
Throw Throwablelnstance;
Здесь Throwablelnstance должен быть объектом типа Throwable или подкласса Throwable. Простые типы, такие как int или char, а также не- Throwable -классы (типа string и object) не могут использоваться как исключения. Имеется два способа получения Throwable -объекта: использование параметра в предложении catch или создание объекта с помощью операции new.
Например:
EOFException e11 = new EOFException("demo");
throw e11;
или
throw new EOFException();
После оператора throw поток выполнения немедленно останавливается, и любые последующие операторы не выполняются. Затем просматривается ближайший включающий блок try с целью поиска оператора catch, который соответствует типу исключения. Если соответствие отыскивается, то управление передается этому оператору. Если нет, то просматривается следующее включение оператора try и т. д. Если соответствующий catch не найден, то программу останавливает обработчик исключений, заданный по умолчанию, и затем выводится трасса стека.
Пример программы, которая создает и вбрасывает исключение (обработчик, который захватывает исключение, перебрасывает его во внешний обработчик): (ThrowDemo.java)
// Демонстрирует throw.
class ThrowDemo {
static void demoproc() {
try {
throw new NullPointerException("demo");
} catch (NullPointerException e) {
System. out. println("Захват внутри demoproc.");
throw e; // повторный выброс исключения
}
}
public static void main(String args[]) {
try {
demoproc ();
} catch (NullPointerException e) {
System. out. println("Повторный захват: " + e);
}
}
}
Эта программа получает две возможности иметь дело с одной и той же ошибкой. Сначала main() устанавливает контекст исключения и затем вызывает demoproc(). Потом метод demoproc() устанавливает другой контекст— для обработки особых ситуаций и немедленно выбрасывает новый экземпляр исключения NullPointerException, который захватывается на следующей строке. Далее это исключение выбрасывается повторно. Итоговый вывод этой программы:
Захват внутри demoproc.
Повторный захват: Java.lang.NullPointerException: demo
Все встроенные исключения времени выполнения имеют два конструктора — один без параметра, а другой — со строчным параметром. Когда используется вторая форма, аргумент определяет строку, описывающую исключение. Данная строка отображается на экран, когда объект указывается в качестве аргумента методами print() или println(). Ее можно также получить с помощью вызова метода getMessage(), который определен в классе Throwable.
Методы с ключевым словом throws
Если метод способен к порождению исключения, которое он не обрабатывает, он должен определить свое поведение так, чтобы вызывающие методы могли сами предохранять себя от данного исключения. Это обеспечивается включением предложения throws в заголовок объявления метода. Предложение throws перечисляет типы исключений, которые метод может выбрасывать. Это необходимо для всех исключений, кроме исключений типа Error, RuntimeException или любых их подклассов. Все другие исключения, которые метод может выбрасывать, должны быть объявлены в предложении throws. Если данное условие не соблюдено, то произойдет ошибка времени компиляции. Общая форма объявления метода, которое включает предложение throws:
type method-name{parameter-list) throw exception-list
{
// тело метода
}
Здесь exception-list — список разделенных запятыми исключений, которые метод может выбрасывать.
Ниже показан пример неправильной программы, пытающейся выбросить исключение, которое она не перехватывает. Поскольку программа не определяет предложение throws, чтобы объявить этот факт, программа не будет компилироваться.
// Эта программа содержит ошибку и не будет компилироваться.
class ThrowsDemo {
static void throwOne() {
System.out.println("Внутри throwOne.");
throw new IllegalAccessException("demo");
}
public static void main(String args[]) {
throwOne();
}
}
Чтобы сделать этот пример компилируемым, требуется внести два изменения. Во-первых, нужно объявить, что throwOne() выбрасывает исключение IllegalAccessException. Во-вторых, main() должен определить оператор try/catch, который захватывает исключение. Исправленный пример выглядит так:
// Теперь эта программа корректна.
class ThrowsDemo {
static void throwOne() throws IllegalAccessException {
System. out. println("Внутри throwOne.");
throw new IllegalAccessException("demo");
}
public static void main(String args[]) {
try {
throwOne ();
} catch (IllegalAccessException e) {
System. out. println("Выброс " + e);
}
}
}
Вывод, сгенерированный выполнением этой программы:
Внутри throwOne.
Выброс Java.lang.IllegalAccessException: demo
Метод должен объявлять все контролируемые исключительные ситуации, которые он может возбудить. Неконтролируемые исключительные ситуации либо находятся вне вашей компетенции (класс Error), либо являются следствием ваших логических ошибок, которые не следовало допускать (класс RuntimeException).
Если ваш метод не сообщает обо всех контролируемых исключительных ситуациях, компилятор выдаст сообщение об ошибке.
Вместо объявления исключительных ситуаций можно их перехватывать. В этом случае исключительная ситуация не возбуждается, и спецификация throws не нужна.
Рассмотрим пример:
Приведенный ниже метод может вызвать исключение связанное с ошибкой ввода/вывода. (FileNotFoundException)
import java.io.*;
public class TExceptWO {
public static void read(String filename) {
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read())!=-1)
{
// Обработка
}
}
public static void main(String args[]) {
String file = "a.b";
read (file);
}
}
В таком виде файл не будет компилироваться
В связи с этим возможны два варианта действий. Первый – использование спецификации throws (см. ниже)
import java.io.*;
public class TExсeptThrows {
public static void read(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read())!=-1)
{
// Обработка
}
}
public static void main(String args[]) {
String file = "a.b";
try {
read (file);
} catch (IOException e1) {
System. out. println("Захват в вызывающем методе: " + e1);
}
}
}
Или
import java.io.*;
public class TExcept {
public static void read(String filename) {
try
{
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read())!=-1)
{
// Обработка
}
}
catch (IOException exception)
{
exception.printStackTrace();
System. out. println(exception.getMessage());
}
}
public static void main(String args[]) {
String file = "a.b";
TExcept. read (file);
}
}
Компилятор строго следит за спецификаторами throws. Вызывая метод, возбуждающий контролируемую исключительную ситуацию, нужно либо самому обрабатывать ее, либо передавать на обработку другому методу. Как правило, следует перехватывать лишь те исключительные ситуации, которые вы сами можете обработать, а остальные передавать дальше. Передавая исключительную ситуацию, нужно добавлять спецификатор throws, чтобы предупредить вызывающий метод.
Обратите внимание на методы, которые могут возбуждать исключительные ситуации. Затем примите решение, обрабатывать исключительную ситуацию или включить в список throws. Во втором варианте нет ничего постыдного - лучше предоставить обработку исключительной ситуации более компетентному обработчику.
Учтите, пожалуйста, что у этого правила есть одно исключение. Если вы создаете метод, который замещает метод суперкласса, не возбуждающий ни одной исключительной ситуации, вы обязаны сами перехватывать каждую проверяемую исключительную ситуацию в коде метода. В такой метод подкласса нельзя добавить спецификатор throws, отсутствующий в методе суперкласса.
Не бойтесь возбуждать или передавать исключительные ситуации для проблем, которые вы не можете решить. С другой стороны, ваши товарищи-программисты возненавидят вас за то, что вы создаете методы, которые перекладывают обработку исключительных ситуаций на их плечи.
Блок finally
Когда исключение возникает, выполнение метода имеет неровный, нелинейный путь, который изменяет нормальное прохождение потока через метод. В зависимости от того, как кодирован метод, исключение может вызвать даже преждевременный выход из него. В некоторых случаях это могло бы стать причиной ошибки. Например, если метод открывает файл для ввода и закрывает его для вывода, то вы вряд ли захотите, чтобы закрывающий файл код был обойден механизмом обработки исключений. Для реализации этой возможности и предназначено ключевое слово finally.
finally определяет блок кода, выполняющийся после того, как блок try/catch завершился и перед кодом, следующим за блоком try/catch. Блок finally будет выполняться независимо от того, было ли выброшено исключение или нет. Если исключение было выброшено, конструкции блока finally будут обрабатываться, даже если нет catch- оператора, соответствующего исключению. Предложение finally необязательно. Однако каждый оператор try требует по крайней мере одного предложения catch или finally.
Например:
// Демонстрирует finally.
class FinallyDemo {
// Выход из метода через исключение.
static void procA() {
try {
System. out. println("Внутри procA");
throw new RuntimeException("demo");
} finally {
System. out. println("finally для procA ");
}
}
// Возврат изнутри try-блока
static void procB() {
try {
System. out. println("Внутри procB");
return;
} finally {
System. out. println("finally для procB ");
}
}
// Нормальное выполнение try-блока.
static void procC() {
try {
System. out. println("Внутри procC");
} finally {
System. out. println("finally procC");
}
}
public static void main(String args[]) {
try {
procA ();
} catch (Exception e) {
System. out. println("Исключение вброшено"); }
procB ();
procC ();
}
}
В этом примере, procA() преждевременно выходит из блока try, выбрасывая исключение. Перед самым выходом выполняется предложение finally. Оператор try метода рrосB() выходит с помощью оператора return. Здесь finally запускается перед возвратом из рrосB(). В методе рrосC() оператор try выполняется нормально, без ошибки. Однако блок finally все же реализуется.
Вывод, сгенерированный предшествующей программой:
Внутри рrосА
finally для рrосА
Исключение выброшено
Внутри рrосВ
finally для рrосВ
Внутри рrосС
finally для рrосС
Подклассы исключений
Исключение | Значение |
ArithmeticException | Арифметическая ошибка типа деления на нуль |
ArraylndexOutOfBoundsException | Индекс массива находится вне границ |
ArrayStoreException | Назначение элементу массива несовместимого типа |
ClassCastException | Недопустимое приведение типов |
IllegalArgirnentException | При вызове метода использован незаконный аргумент |
IllegalMonitorStateException | Незаконная операция монитора, типа ожидания на разблокированном потоке |
IllegalStateException | Среда или приложение находятся в некорректном состоянии |
IllegalThreadStateException | Требуемая операция не совместима с текущим состоянием потока |
IndexOutOfBoundsException | Некоторый тип индекса находится вне границ |
NegativeArraySizeException | Массив создавался с отрицательным размером |
NullPointerException | Недопустимое использование нулевой ссылки |
NumberFormatException | Недопустимое преобразование строки в числовой формат |
SecurityException | Попытка нарушить защиту |
StringIndexOutOfBoundsException | Попытка индексировать вне границ строки |
OnsupportedOperationE'xception | Встретилась неподдерживаемая операция |
Исключение | Значение |
ClassNotFoundException | Класс не найден |
CloneNotSupportedException | Попытка клонировать объект, который не реализует интерфейс Cloneable |
IllegalAccessException | Доступ к классу отклонен |
InstantiationException | Попытка создавать объект абстрактного класса или интерфейса |
InterruptedException | Один поток был прерван другим потоком |
NoSuchFieldException | Требуемое поле не существует |
NoSuchMethodException | Требуемый метод не существует |