Глава 8
ИСКЛЮЧЕНИЯ И ОШИБКИ
Существуют только ошибки.
Аксиома Робертса
Что для одного ошибка, для другого —
исходные данные.
Следствие Бермана из аксиомы Робертса
Никогда не выявляйте в программе ошибки,
если не знаете, что с ними делать.
Руководство Штейнбаха
Иерархия исключений и ошибок
Исключительные ситуации (исключения) и ошибки возникают во время вы-
полнения программы, когда появившаяся проблема не может быть решена в те-
кущем контексте и невозможно продолжение работы программы. Примерами
«популярных» ошибок являются: попытка индексации вне границ массива, вы-
зов метода на нулевой ссылке или деление на нуль. При возникновении исклю-
чения в приложении создается объект, описывающий это исключение. Затем те-
кущий ход выполнения приложения останавливается, и включается механизм
обработки исключений. При этом ссылка на объект-исключение передается об-
работчику исключений, который пытается решить возникшую проблему и про-
должить выполнение программы. Если в классе используется метод, в котором
может возникнуть проверяемая исключительная ситуация, но не предусмотре-
на ее обработка, то ошибка возникает еще на этапе компиляции. При создании
такого метода программист обязан включить в код метода обработку исключе-
ний, которые могут генерироваться в этом методе, или передать обработку
исключения на более высокий уровень методу, вызвавшему данный метод.
Исключение не должно восприниматься как нечто вредное, от которого сле-
дует избавиться любой ценой. Исключение — это источник дополнительной
информации о ходе выполнения приложения. Такая информация позволяет
лучше адаптировать код к конкретным условиям его использования, а также
на ранней стадии выявить ошибки или защититься от их возникновения в бу-
дущем. В противном случае «подавление» исключений приведет к тому, что
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
202
о возникшей ошибке никто не узнает или узнает
на стадии некорректно обработанной информа-
ции. Поиск места возникновения может быть
затруднительным.
Каждой исключительной ситуации постав-
лен в соответствие некоторый класс, экземпляр
которого инициируется при исключительной
ситуации. Если подходящего класса не сущест-
вует, то он может быть создан разработчиком.
Все исключения являются наследниками су-
перкласса Throwable и его подклассов Error
и Exception из пакета java.lang.
Исключительные ситуации типа Error возникают только во время выполне-
ния программы. Такие исключения связаны с серьезными ошибками, к приме-
ру, с переполнением стека, не подлежат исправлению и не могут обрабатывать-
ся приложением. Некоторые классы из иерархии наследуемых от класса Error
приведены на рис. 8.2.
Ниже приведена иерархия классов проверяемых исключений, наследуемых
от класса Exception при отсутствии в цепочке наследования класса
RuntimeException. Возможность возникновения проверяемого исключения
может быть отслежена еще на этапе компиляции кода. Компилятор проверяет,
может ли данный метод генерировать или обрабатывать исключение.
Проверяемые исключения должны быть обработаны в методе, который мо-
жет их генерировать, или включены в throws-список метода для дальнейшей
обработки в вызывающих методах.
Во время выполнения могут генерироваться также исключения, которые
могут быть обработаны без ущерба для выполнения программы. Список этих
исключений приведен на рис. 8.4. В отличие от проверяемых исключений, класс
RuntimeException и порожденные от него классы относятся к непрове ряемым
исключениям. Компилятор не проверяет, может ли генерировать и/или обраба-
тывать метод эти исключения. Исключения типа RuntimeException генериру-
ются при возникновении ошибок во время выполнения приложения.
Рис. 8.1.
Иерархия основных
классов исключений
Рис. 8.2.
Некоторые классы исключений, наследуемые от класса Error
ИСКЛЮЧЕНИЯ И ОШИБКИ
203
Почему возникла необходимость деления исключений на проверяемые
и непроверяемые? Представим, что следующие ситуации проверяются на этапе
компиляции, а именно:
• деление в целочисленных типах вида a/b при b=0 генерирует исключение
ArithmeticException;
• индексация массивов. Выход за пределы массива приводит к исключению
ArrayIndexOfBoundException;
• вызов метода на ссылке вида obj.toString(), если obj ссылается на null.
Если бы возможность появления перечисленных исключений проверялась
на этапе компиляции, то любая попытка индексации массива или каждый вызов
метода требовали бы или блока try-catch, или секции throws. Такой код был бы
практически непригоден для понимания и поддержки, поэтому часть исключе-
ний была выделена в группу непроверяемых и ответственность за защиту прило-
жения от последствий их возникновения возложена на программиста.
Ниже приведен список часто встречаемых в практике программирования
непроверяемых исключений, знание причин возникновения которых необхо-
димо при создании качественного кода.
Рис. 8.3.
Иерархия классов проверяемых (checked) исключительных ситуаций
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
204
Способы обработки исключений
Если при возникновении исключения в текущем методе обработчик не будет
обнаружен, то его поиск будет продолжен в методе, вызвавшем данный метод,
и так далее вплоть до метода main() для консольных приложений или другого
метода, запускающего соответствующий вид приложения. Если же и там исключе-
ние не будет перехвачено, то JVM выполнит аварийную остановку приложения
с вызовом метода printStackTrace(), выдающего данные трассировки.
Для проверяемого исключения возможность его генерации отслеживается.
Передача обработки вызывающему методу осуществляется с помощью опера-
тора throws. В конце концов исключение будет передано в метод main(), где
и должна находиться крайняя точка обработки. Добавлять оператор throws ме-
тоду main() представляется дурным тоном программирования, как безответст-
венное действие программиста, не обращающего никакого внимания на аль-
тернативное выполнение программы.
На практике используется один из трех способов обработки исключений:
• перехват и обработка исключения в блоке try-catch метода;
• объявление исключения в секции throws метода и передача вызывающему
методу (в первую очередь для проверяемых исключений);
• использование собственных исключений.
Первый подход можно рассмотреть на следующем примере. При преобразова-
нии содержимого строки к числу в определенных ситуациях может возникать прове-
ряемое исключение типа ParseException. Например:
Исключение
Значение
ArithmeticException
Арифметическая ошибка: деление на нуль и др.
ArrayIndexOutOfBoundsException Индекс массива находится вне границ
ArrayStoreException
Назначение элементу массива несовместимого типа
ClassCastException
Недопустимое приведение типов
ConcurrentModificationException
Некорректная модификация коллекции
IllegalArgumentException
При вызове метода использован незаконный аргумент
IllegalMonitorStateException
Незаконная операция монитора на разблокированном экземпляре
IllegalStateException
Среда или приложение находятся в некорректном состоянии
IllegalThreadStateException
Требуемая операция не совместима с текущим состоянием потока
IndexOutOfBoundsException
Некоторый тип индекса находится вне границ
NegativeArraySizeException
Массив создавался с отрицательным размером
NullPointerException
Недопустимое использование нулевой ссылки
NumberFormatException
Недопустимое преобразование строки в числовой формат
StringIndexOutOfBoundsException
Попытка индексации вне границ строки
UnsupportedOperationException
Встретилась неподдерживаемая операция
Рис. 8.4.
Классы непроверяемых исключений, наследуемых от класса RuntimeException
ИСКЛЮЧЕНИЯ И ОШИБКИ
205
public double
parseFromFrance(String numberStr) {
NumberFormat nfFr = NumberFormat. getInstance(Locale. FRANCE);
try
{
double numFr = nfFr.parse(numberStr).doubleValue();
return numFr;
} catch (ParseException e) { // проверяемое исключение
// 1. генерация стандартного исключения, н-р: IllegalArgumentException() — не очень хорошо
// 2. генерация собственного исключения
// 3. return 0 или другого значения по умолчанию; — нежелательно
}
}
Исключительная ситуация возникнет в случае, если переданная строка со-
держит нечисловые символы или не является числом. Генерируется объект
исключения, и управление передается соответствующему блоку catch, в кото-
ром он обрабатывается, иначе блок catch пропускается. Блок try похож
на обычный логический блок. Блок catch(){} похож на метод, принимающий
в качестве единственного параметра ссылку на объект-исключение и обраба-
тывающий этот объект.
Второй подход демонстрируется на этом же примере. Метод может генери-
ровать исключения, которые сам не обрабатывает, а передает для обработки дру-
гим методам, вызывающим данный метод. В этом случае метод должен объявить
о таком поведении с помощью ключевого слова throws, чтобы вызывающий ме-
тод мог защитить себя от этих исключений. В вызывающем методе должна
быть предусмотрена или обработка этих исключений, или последующая пере-
дача вызывающему методу.
При этом сам таким образом объявляемый метод может содержать блоки
try-catch, а может и не содержать их. Например, метод parseFromFrance()
можно объявить:
public double
parseFromFrance(String numberStr) throws ParseException {
NumberFormat nfFr = NumberFormat. getInstance(Locale. FRANCE);
double
numFr = nfFr.parse(numberStr).doubleValue();
return
numFr;
}
Ключевое слово throws позволяет разобраться с исключениями методов «чу-
жих» классов, код которых отсутствует. Обрабатывать исключение при этом
должен будет метод, вызывающий parseFromFrance():
public
void doAction() {
// same code here
try
{
��parseFromFrance(numberStr);
}
catch
(ParseException e) {
// обработка
}
}
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
206
Создание и применение собственных исключений будет рассмотрено позже
в этой главе.
Обработка нескольких исключений
Если в блоке try может быть сгенерировано в разных участках кода несколь-
ко типов исключений, то необходимо наличие нескольких блоков catch, если
только блок catch не обрабатывает все типы исключений.
/* # 1 # обработка двух типов исключений # TwoExceptionAction.java */
package
by.bsu.exception;
public class
TwoExceptionAction {
public
void doAction() {
try
{
int
a = (int)(Math.random() * 2);
System.out.println("a = " + a);
int c[] = { 1/a }; // опасное место #1
c[a] = 71; // опасное место #2
}
catch
(ArithmeticException e) {
System.err.println("деление на 0" + e);
}
catch
(ArrayIndexOutOfBoundsException e) {
System.err.println("out of bound: " + e);
} // окончание try-catch блока
System.out.println("after try-catch");
}
}
Исключение «деление на 0» возникнет при инициализации элемента масси-
ва а=0. В противном случае (при а=1) генерируется исключение «превышение
границ массива» при попытке присвоить значение второму элементу массива
с[], который содержит только один элемент. Однако пример, приведенный
выше, носит чисто демонстративный характер и не является образцом хоро-
шего кода, так как в этой ситуации можно было обойтись простой проверкой
аргументов на допустиые значения перед выполнением операций. К тому же
генерация и обработка исключения — операция значительно более ресурсо-
емкая, чем вызов оператора if для проверки аргумента. Исключения должны
применяться только для обработки исключительных ситуаций, и если суще-
ствует возможность обойтись без них, то следует так и поступить.
Подклассы исключений в блоках catch должны следовать перед любым
из их суперклассов, иначе суперкласс будет перехватывать эти исключения.
Например:
try
{ /* код, который может вызвать исключение */
} catch(IllegalArgumentException e) {
} catch(PatternSyntaxException e) { } /* никогда не может быть вызван: ошибка компиляции */
ИСКЛЮЧЕНИЯ И ОШИБКИ
207
где класс PatternSyntaxException представляет собой подкласс класса
IllegalArgumentException. Корректно будет просто поменять местами блоки
catch:
try
{ /* код, который может вызвать исключение */
} catch(PatternSyntaxException e) {
} catch(IllegalArgumentException e) {
}
На практике иногда возникают ситуации, когда инструкций catch несколько
и обработка производится идентичная, например, вывод сообщения об исклю-
чении в журнал.
try
{
// some operations
} catch(NumberFormatException e) {
e.printStackTrace();
} catch(ClassNotFoundException e) {
e.printStackTrace();
} catch(InstantiationException e) {
e.printStackTrace();
}
В версии Java 7 появилась возможность объединить все идентичные ин-
струкции в одну, используя для разделения оператор «|».
try
{
// some operations
} catch(NumberFormatException | ClassNotFoundException | InstantiationException e) {
e.printStackTrace();
}
Такая запись позволяет избавиться от дублирования кода.
Введено понятие более точной переброски исключений (more precise re-
throw). Это решение применимо в случае, если обработка возникающих исклю-
чений не предусматривается в методе и должна быть передана вызывающему
данный метод методу.
До введения этого понятия код выглядел так:
public
double parseFromFileBefore(String filename)
throws FileNotFoundException, ParseException, IOException {
NumberFormat nfFr = NumberFormat.getInstance(Locale.FRANCE);
double numFr = 0;
BufferedReader buff = null;
try {
FileReader
fr
=
new
FileReader(filename);
buff
=
new
BufferedReader(fr);
String number = buff.readLine();
numFr
=
nfFr.parse(number).doubleValue();
}
catch
(FileNotFoundException e) {
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
208
throw
e;
}
catch
(IOException e) {
throw
e;
}
catch
(ParseException e) {
throw
e;
}
finally
{
if
(buff != null) {
buff.close();
}
}
return
numFr;
}
More precise rethrow разрешает записать в единственную инструкцию catch
более общее исключение, чем может быть генерировано в инструкции try,
с последующей генерацией перехваченного исключения для его передачи в вы-
зывающий метод.
public
double parseFromFile(String filename)
throws
FileNotFoundException, ParseException, IOException {
NumberFormat nfFr = NumberFormat.getInstance(Locale.FRANCE);
double
numFr = 0;
BufferedReader buff = null;
try
{
FileReader
fr
=
new
FileReader(filename);
buff
=
new
BufferedReader(fr);
String number = buff.readLine();
numFr
=
nfFr.parse(number).doubleValue();
}
catch
(final Exception e) { // final — необязателен
throw
e; // more precise rethrow
}
finally
{
if
(buff != null) {
buff.close();
}
}
return
numFr;
}
Наличие секции throws контролируется компилятором на предмет точного
указания списка проверяемых исключений, которые могут быть генерированы
в блоке try-catch. При возможности возникновения непроверяемых исключе-
ний последние в секции throws обычно не указываются. Ключевое слово final
не позволяет подменить экземпляр исключения для передачи за пределы мето-
да. Однако данную конструкцию можно использовать и без final.
Операторы try можно вкладывать друг в друга. Если у оператора try низко-
го уровня нет раздела catch, соответствующего возникшему исключению,
ИСКЛЮЧЕНИЯ И ОШИБКИ
209
поиск будет развернут на одну ступень выше, и будут проверены разделы catch
внешнего оператора try.
/* # 2 # вложенные блоки try-catch # NestedTryCatchRunner.java */
package
by.bsu.exception;
public class
NestedTryCatchRunner {
public
void doAction() {
try
{ // внешний блок
int
a = (int) (Math.random() * 2) — 1;
System.out.println("a = " + a);
try
{ // внутренний блок
int
b = 1/a;
StringBuilder
sb
=
new
StringBuilder(a);
} catch (NegativeArraySizeException e) {
System.err.println("недопустимый размер буфера: " + e);
}
}
catch
(ArithmeticException e) {
System.err.println("деление на 0: " + e);
}
}
}
В результате запуска приложения при a=0 будет сгенерировано исключение
ArithmeticException, а подходящий для его обработки блок try–catch является
внешним по отношению к месту генерации исключения. Этот блок и будет за-
действован для обработки возникшей исключительной ситуации. Вкладывание
блоков try-catch друг в друга загромождает код, поэтому такими конструкция-
ми следует пользоваться с осторожностью.
Оператор throw
При разработке кода возникают ситуации, когда в приложении необходимо
инициировать генерацию исключения для указания, напри мер, на заведомо
ошибочный результат выполнения операции, на некорректные значения пара-
метра метода и др. Для генерации исключительной ситуации и создания экзем-
пляра исключения используется оператор throw. В качестве исключения дол-
жен быть использован объект подкласса класса Throwable, а также ссылки
на них. Общая форма записи инструкции throw, генерирующей исключение:
throw
объектThrowable;
Объект-исключение может уже существовать или создаваться с помощью
оператора new:
throw new
IIlegalArgumentException();
При достижении оператора throw выполнение кода прекращается. Ближайший
блок try проверяется на наличие соответствующего обработчика catch. Если
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
210
он существует, управление передается ему, иначе проверяется следующий
из вложенных операторов try. Инициализация объекта-исключения без опера-
тора throw никакой исключительной ситуации не вызовет.
В ситуации, когда получение методом достоверной информации критично
для выполнения им своей функциональности, у программиста может возникнуть
необходимость в генерации исключения, так как метод не может выполнить ожи-
даемых от него действий, основываясь на некорректных или ошибочных данных.
Ниже приведен пример, в котором сначала создается объект-исключение,
затем оператор throw генерирует исключение, обрабатываемое в разделе catch,
в котором генерируется другое исключение.
/* # 3 # генерация исключений # Connector.java # Runner.java # SameResource.java */
package
by.bsu.conn;
public
class Connector {
public static
void loadResource(SameResource f) {
if
(f == null || !f.exists() || !f.isCreate()) {
throw
new IllegalArgumentException(); /* генерация исключения */
// или собственное, н-р, throw new IllegalResourceException();
}
// more code
}
}
package
by.bsu.conn;
public class
Runner {
public
static void main(String[ ] args) {
SameResource f = new SameResource(); // SameResource f = null;
try
{ // необязателен только при гарантированной корректности значения параметра
Connector. loadResource(f);
}
catch
(IllegalArgumentException e) {
System. err.print("обработка unchecked-исключения вне метода: " + e);
}
}
package
by.bsu.conn;
public
class SameResource {
// поля, конструкторы
public boolean isCreate() {
// more code
}
public boolean exists() {
// more code
}
public
void execute() {
// more code
}
public void close() {
// more code
}
}
ИСКЛЮЧЕНИЯ И ОШИБКИ
211
Вызываемый метод loadResource() может (при отсутствии требуемого ресурса
или при аргументе null) генерировать исключение, перехватываемое обработчиком.
В результате экземпляр непроверяемого исключения IllegalArgumentException как
подкласса класса RuntimeException передается обработчику исключений
в методе main().
В случае генерации проверяемого исключения IllegalResourceException ком-
пилятор требует обра ботки объекта исключения в методе или передачи его с по-
мощью инструкции throws. Проверяемое исключение может быть создано как
public class
IllegalResourceException extends Exception {}
Тогда методы loadResource() и main() будут выглядеть так:
public static
void loadResource(SameResource f) throws IllegalResourceException {
if
(f == null || !f.exists() || !f.isCreate()) {
throw
new IllegalResourceException();
}
// more code
}
В этом случае в методе main() блок try-catch будет обязателен.
Если метод генерирует исключение с помощью оператора throw и при этом
блок catch в методе отсутствует, то для передачи обработки исключения вызыва-
ющему методу тип проверяемого (checked) класса исключений должен быть ука-
зан в операторе throws при объявлении метода. Для исключений, являющихся
подклассами класса RuntimeException (unchecked) и используемых для отобра-
жения программных ошибок, при выполнении приложения throws в объявлении
может отсутствовать, так как играет только информационную роль.
Блок finally
Возможна ситуация, при которой нужно выполнить некоторые действия
по завершению программы (закрыть поток, освободить соединение с базой
данных) вне зависимости от того, произошло исключение или нет. В этом
случае используется блок finally, который обязательно выполняется после ин-
струкций try или catch. Например:
try
{ /* код, который может вызвать исключение */
} catch(OneClassException e) { /* обработка исключения */ // необязателен
} catch(TwoClassException e) { /* обработка исключения */ // необязателен
} finally { /* выполняется или после try, или после catch */ }
Каждому разделу try должен соответствовать по крайней мере один раздел
catch или блок finally. Блок finally часто используется для закрытия файлов
и освобождения других ресурсов, захваченных для временного использования
в начале выполнения метода. Код блока выполняется перед выходом из метода
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
212
даже в том случае, если перед ним были выполнены инструкции вида return,
break, continue.
/* # 4 # выполнение блоков finally # ResourceAction.java */
package
by.bsu.conn;
public class
ResourceAction {
public void
doAction() {
SameResource sr = null;
try
{
// реализация — захват ресурсов
sr = new SameResource(); // возможна генерация исключения
// реализация — использование ресурсов
sr.execute(); // возможна генерация исключения
// sr.close(); // освобождение ресурсов (некорректно)
}
finally
{
// освобождение ресурсов (корректно)
if (sr != null) {
sr.close();
}
}
System. out.print("after finally");
}
}
В методе doAction() при использовании ресурсов и генерации исключе-
ния осуществляется преждевременный выход из блока try с игнорировани-
ем всего оставшегося в нем кода, но до выхода из метода обязательно будет
выполнен раздел finally. Освобождение ресурсов в этом случае произойдет
корректно. В следующей главе будет рассмотрен новый способ закрытия
ресурсов autocloseable.
Собственные исключения
Для повышения качества и скорости восприятия кода разработчик мо-
жет создать собственное исключение как подкласс класса Exception и затем
использовать его при обработке ситуации, не являющейся исключением с точ-
ки зрения языка, но нарушающей логику вещей. По соглашению наcледник
любого класса-исключения должен заканчиваться словом Exception.
Например, возможность появления объекта типа Coin с отрицательным
значением поля diameter является предлогом для генерации собственного
логического исключения CoinLogicException, хотя для языка Java появле-
ние у объекта поля с отрицательным значением исключением не является
и впоследствии к возникновению других исключений само по себе привес-
ти не может.
ИСКЛЮЧЕНИЯ И ОШИБКИ
213
/* # 5 # метод, вызывающий исключение, созданное программистом # Coin.java */
package
by.bsu.fund.entity;
import
by.bsu.fund.exceptions.CoinLogicException;
public
class Coin {
private
double diameter;
private
double weight;
public
double getDiameter() {
return diameter;
}
public
void setDiameter(double value) throws CoinLogicException {
if(value <= 0) {
throw new CoinLogicException("diameter is incorrect");
}
diameter = value;
}
public
double getWeight() {
return weight;
}
public
void setWeight(double value) {
weight = value;
}
}
При невозможности присвоить значение генерируется экземпляр
CoinLogicException, используемый в качестве собственного исключения.
/* # 6 # собственное «логическое» исключение # CoinLogicException.java */
package
by.bsu.fund.exceptions;
public
class CoinLogicException extends Exception {
public
CoinLogicException() {
}
public
CoinLogicException(String message, Throwable exception) {
super(message, exception);
}
public
CoinLogicException(String message) {
super(message);
}
public
CoinLogicException (Throwable exception) {
super(exception);
}
}
Если же генерируется стандартное исключение или получены значения неко-
торых параметров — такие, что генерация какого-либо стандартного исключе-
ния становится неизбежной немедленно либо сразу по выходе из метода, то сле-
дует генерировать собственное техническое исключение CoinTechnicalException.
Продемонстрировать процесс генерации и обработки логического и техни-
ческого собственных исключений можно на примере.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
214
/* # 7 # генерация и обработка собственных исключений */
public
void doAction(String value) throws CoinTechnicalException {
Coin ob = new Coin();
try {
double d = Double.parseDouble(value);
ob.setDiameter(d);
}
catch
(NumberFormatException e) {
throw new CoinTechnicalException("incorrect symbol in string", e);
}
catch
(CoinLogicException e) {
System.err.println(e.getCause());
}
}
У класса-наследника Exception обычно определяются четыре конструктора,
два из которых в качестве параметра принимают объект типа Throwable, что озна-
чает генерацию исключения на основе другого исключения. Такая ситуация в при-
веденном случае возможна, например, при предварительном определении диаме-
тра монеты, в процессе которого произошла ошибка преобразования строки
в базовый тип, спровоцировавшая возникновение NumberFormatException.
Значение диаметра монеты присвоить все равно невозможно, поэтому есть
смысл для более точной передачи причин некорректной работы приложения
сгенерировать CoinTechnicalException, но с вызовом конструктора, обладаю-
щего параметром типа Throwable.
/* # 8 # собственное «техническое» исключение # CoinTechnicalException.java */
package
by.bsu.fund.exceptions;
public
class CoinTechnicalException extends Exception {
public
CoinTechnicalException() {
}
public
CoinTechnicalException(String message, Throwable cause) {
super(message, cause);
}
public
CoinTechnicalException(String message) {
super(message);
}
public
CoinTechnicalException(Throwable cause) {
super(cause);
}
}
Один из них — сообщение, которое может быть выведено в поток ошибок;
другой — реальное исключение, которое привело к вызову технического
исключения. Этот код показывает, как можно сохранить дополнительную ин-
формацию внутри пользовательского исключения. Преимущество этого сохра-
нения состоит в том, что если вызываемый метод захочет узнать реальную при-
чину вызова CoinTechnicalException, он всего лишь должен вызвать метод
ИСКЛЮЧЕНИЯ И ОШИБКИ
215
getCause(). Это позволяет вызываемому методу решить, нужно ли работать со
специфичным исключением или достаточно обработки CoinTechnicalException.
Разработчики программного обеспечения стремятся к высокому уровню
повторного использования кода, поэтому они постарались предусмотреть
и закодировать все возможные исключительные ситуации. При реальном
программировании создание собственных классов исключений позволяет
разработчику выделить важные аспекты приложения и обратить внимание
на детали разработки.
Приведенные выше классы собственных исключений могут иметь общий
суперкласс, например: CoinException, что позволит всегда определить, являет-
ся ли перехваченное исключение собственным или стандартным. Тогда преды-
дущий пример можно переписать в виде:
/* # 9 # генерация и переброска собственных исключений # */
public
void doAction(String value) throws CoinLogicException {
Coin ob = new Coin();
try {
double d = Double.parseDouble(value);
ob.setDiameter(d);
}
catch
(CoinException e) {
throw e;
}
}
Наследование и исключения
Создание сложных распределенных систем редко обходится без наследова-
ния и обработки исключений. Следует знать два правила для проверяемых
исключений при наследовании:
• переопределяемый метод в подклассе не может содержать в инструк ции
throws исключений, не обрабатываемых в соответствующем методе супер-
класса;
• конструктор подкласса должен включить в свой блок throws все классы
исключений или их суперклассы из блока throws конструк тора суперкласса,
к которому он обращается при создании объекта.
Первое правило имеет непосредственное отношение к расширяемости при-
ложения. Пусть при добавлении в цепочку наследования нового класса его по-
лиморфный метод включил в блок throws «новое» проверяемое исключение.
Тогда методы логики приложения, принимающие объект нового класса в каче-
стве параметра и вызывающие данный полиморфный метод, не готовы обраба-
тывать «новое» исключение, так как ранее в этом не было необходимости.
Поэтому при попытке добавления «нового» checked-исключения в полиморф-
ный метод компилятор выдает сообщение об ошибке.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
216
/* # 10 # полиморфизм и исключения # Stone.java # WhiteStone.java # BlackStone.java #
StoneAction.java */
package
by.bsu.polymorph;
public
class Stone { // ранее созданный класс
public
void build(String data) throws ParseException {
/* реализация */
}
}
package
by.bsu.polymorph;
public
class WhiteStone extends Stone { // ранее созданный класс
@Override
public
void build(String data) {
/*
реализация */
System. out.println("белый каменный шар");
}
}
package
by.bsu.polymorph;
public
class StoneAction { // ранее созданный класс
public
void buildHouse(Stone stone) {
try
{
stone.build("some
info");
// предусмотрена обработка ParseException и его подклассов
}
catch
(ParseException e) {
System. err.print(e);
}
}
}
package
by.bsu.polymorph;
public
class BlackStone extends Stone { // новый класс
@Override
public
void build(String data) throws Exception { // ошибка компиляции
System. out.println("черный каменный шар");
/* реализация*/
}
}
Если же при объявлении метода суперкласса инструкция throws присутст-
вует, то в подклассе эта инструкция может вообще отсутствовать или в ней
могут быть объявлены любые исключения, являющееся подклассами исключе-
ния из блока throws метода суперкласса.
Второе правило позволяет защитить программиста от возникновения неиз-
вестных ему исключений при создании объекта.
/* # 11 # конструкторы и исключения # Resource.java # ConcreteResource.java */
package
by.bsu.construction;
import
java.io.FileNotFoundException;
import
java.io.IOException;
ИСКЛЮЧЕНИЯ И ОШИБКИ
217
class
Resource { // ранее созданный класс
public
Resource(String filename) throws FileNotFoundException {
// more code
}
}
class
ConcreteResource extends Resource { // ранее созданный класс
//
ранее созданный конструктор
public
ConcreteResource(String name) throws FileNotFoundException {
super
(name);
//
more code
}
//
ранее созданный конструктор
public
ConcreteResource() throws IOException {
super
("file.txt");
// more code
}
//
новый конструктор
public
ConcreteResource(String name, int mode) { /* ошибка компиляции */
super
(name);
// more code
}
public
ConcreteResource(String name, int mode, String type) throws ParseException {
/* ошибка компиляции */
super
(name);
// more code
}
}
Если разрешить создание экземпляра в виде
ConcreteResource inCorrect = new ConcreteResource("info", 1);
то конструктор суперкласса может генерировать исключение и никаких пред-
варительных действий по его предотвращению принято не будет.
try
{
ConcreteResource correct = new ConcreteResource();
} catch (IOException e) {
// обработка
}
В приведенном выше случае компилятор не разрешит создать конструктор
подкласса, обращающийся к конструктору суперкласса без корректной инструк-
ции throws. Если бы это было возможно, то при создании объекта подкласса
класса ConcreteResource не было бы никаких сообщений о возможности гене-
рации исключения, и при возникновении исключительной ситуации ее источ-
ник было бы трудно идентифицировать.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
218
Рекомендации по обработке исключений
В любом случае, если есть возможность не генерировать исключение, сле-
дует ею воспользоваться. Генерация исключения — процесс ресурсоемкий,
и слишком частая генерация исключений оказывает влияние на быстродействие.
• Не обрабатывать конкретное исключение или несколько исключений с ис-
пользованием в инструкции catch исключения более общего типа.
try
{
// some code here
int
a = Integer. parseInt(args[0]);
StringBuilder sb = new StringBuilder(a);
// some code here
} catch (Exception e) {
System. err.println(e);
}
Следует классифицировать исключения. Вместо этого следует использовать:
try
{
int
a = Integer. parseInt(args[0]);
StringBuilder sb = new StringBuilder(a);
} catch (NegativeArraySizeException e) {
System. err.println("недопустимый размер буфера: " + e);
} catch (NumberFormatException e) {
System. err.println("недопустимый символ в числе: " + e);
}
• Не оставлять пустыми блоки catch. При генерации и перехвате исключения
никто не узнает, что исключительная ситуация имела место, и не станет
устранять ее причины
try
{
// some code here
} catch (NumberFormatException e) {
}
• По возможности не использовать одинаковую обработку различных исклю-
чений
try
{
// some code here
} catch (IOException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
}
Для замены можно воспользоваться конструкцией multi-catch из Java 7 или
в каждую инструкцию catch помещать уникальную обработку.
• Не создавать класс исключений, эквивалентный по смыслу уже существующему.
Прежде чем написать свое исключение, необходимо изучить документацию,
ИСКЛЮЧЕНИЯ И ОШИБКИ
219
возможно, там найдется что-то подходящее. Например, вместо того, чтобы
создавать исключение для информирования о некорректной работе с пере-
числением вида
public
class EnumNotPresentException extends Exception { }
следует применить класс EnumConstantNotPresentException.
• Не создавать избыточное число классов собственных исключений. Прежде
чем создавать новый класс исключений, следует подумать, что, возможно,
ранее созданный в состоянии его обработать.
• Не использовать исключения, которые могут ввести в заблуждение:
public
class Human {
private
int year;
public
void setYear(int year) throws IOException {
if
(year <= 0) {
throw
new IOException();
}
this
.year = year;
}
}
• Не допускать, чтобы часть обработки ошибки присутствовала в блоке, гене-
рирующем исключение:
public
void setDeduce(double deduce) throws TaxException {
if
(deduce < 0) {
this
.deduce = 0; // лишнее
recalculateAmount();
// совсем лишнее
System.err.print(DEDUCE_NEGATIVE);
throw
new TaxException("VAT deduce < 0");
}
this
.deduce = deduce;
recalculateAmount();
}
• Никогда самостоятельно не генерировать NullPointerException и избегать
случаев, когда такая генерация возможна в принципе. Проверка значения
ссылки на null позволяет обойтись без генерации исключения. Если по ло-
гике приложения необходимо генерировать исключение, следует использо-
вать, например, IllegalArgumentException с соответствующей информаци-
ей об ошибке или собственное исключение.
• Не следует в общем случае в секцию throws помещать unchecked-исключения.
• Не рекомендуется вкладывать блоки try-catch друг в друга из-за ухудшения
читаемости кода.
• При создании собственных исключений следует проводить наследование
от класса Exception, либо от другого проверяемого класса исключений, а не
от RuntimeException.
• Никогда не генерировать исключения в инструкции finally:
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
220
try
{
// some code here
} finally {
if
(условие) {
throw new
ParseException();
}
}
При такой генерации исключения никто в приложении не узнает об исклю-
чении и, соответственно, не сможет обработать исключение, ранее сгенери-
рованное в инструкции try, в случае, если оно не было обработано в инструк-
ции catch. В связи со сказанным никогда не следует использовать в инструкции
finally операторы return, break, continue.
Отладочный механизм assertion
Борьба за качество программ ведется всеми возможными способами. На этапе
отладки найти неявные ошибки в функционировании приложения бывает доволь но
сложно. Например, в методе, устанавливающем возраст пользователя, информация
о возрасте извлекается из внешних источников (файл, БД), и в результате получает-
ся отрицательное значение. Далее неверные данные влияют на результат вычисле-
ния среднего возраста пользователей и т. д. Определять и исправ лять такие ситуа-
ции позволяет механизм проверочных утверждений (assertion). При помощи этого
механизма можно сформулировать требования к входным, выходным и промежу-
точным данным методов классов в виде некоторых логических условий.
Попытка обработать ситуацию появления отрицательного возраста может
выглядеть следующим образом:
int
age = ob.getAge();
if
(age >= 0) {
// more code
} else {
// сообщение о неправильных данных
}
Теперь механизм assertion позволяет создать код, который будет генериро-
вать исключение на этапе отладки проверки постусловия или промежуточных
данных в виде:
int
age = ob.getAge();
assert
(age >= 0): "NEGATIVE AGE!!!";
// more code
Правописание инструкции assert:
assert
boolexp : expression;
assert
boolexp;
ИСКЛЮЧЕНИЯ И ОШИБКИ
221
Выражение boolexp может принимать только значение типов boolean или
Boolean, а expression — любое значение, которое может быть преобра зовано
к строке. Если логическое выражение получает значение false, то гене рируется
исключение AssertionError и выполнение программы прекращается с выво-
дом на консоль значения выражения expression (если оно задано).
Механизм assertion хорошо подходит для проверки инвариантов, например,
перечислений:
enum
Mono { WHITE, BLACK }
String str = "WHITE"; // "GRAY"
Mono mono = Mono.valueOf(str);
// more code
switch
(mono) {
case
WHITE : // more code
break;
case
BLACK : // more code
break;
default
:
assert false : "Colored!";
}
Создатели языка не рекомендуют использовать assertion при проверке парамет-
ров public-методов. В таких ситуациях лучше обрабатывать возможность генера-
ции исключения одного из типов: IllegalArgumentException, NullPointerException
или собственное исключение. Нет также особого смысла в механизме assertion
при проверке пограничных значений переменных, поскольку исключительные
ситуации генерируются в этом случае без посторонней помощи.
Механизм assertion можно включать для отдельных классов и пакетов при
запуске виртуальной машины в виде:
java -enableassertions RunnerClass
или
java -ea RunnerClass
Для выключения применяется -da или -disableassertions.
Задания к главе 8
Вариант A
Выполнить задания на основе варианта А гл. 4, контролируя состояние пото-
ков ввода/вывода. При возникновении ошибок, связанных с корректностью вы-
полнения математических операций, генерировать и обрабатывать ис клю-
читель ные ситуации. Предусмотреть обработку исключений, возникающих при
нехватке памяти, отсутствии требуемой записи (объекта) в файле, недопусти-
мом значении поля и т. д.
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
222
Вариант B
Выполнить задания из варианта В гл. 4, реализуя собственные обработчики
исключений и исключения ввода/вывода.
Тестовые задания к главе 8
Вопрос 8.1.
Выберите правильные утверждения (3):
1) Проверяемые (checked) исключения являются наследниками класса
java.lang.Exception
2) Непроверяемые (unchecked) исключения являются наследниками класса
java.lang.Error
3) Непроверяемые (unchecked) исключения являются наследниками класса
java.lang.Exception
4) Проверяемые (checked) исключения обязательно обрабатываются
5) Непроверяемые (unchecked) исключения невозможно обработать
Вопрос 8.2.
Дан код:
try { FileReader fr1 = new FileReader("test1.txt");
try { FileReader fr2 = new FileReader("test2.txt");
} catch (IOException e) {
System. out.print("test2");
}
System. out.print("+");
} catch (FileNotFoundException e) {
System. out.print("test1");
}
System. out.print("+");
Какая строка выведется на консоль при компиляции и запуске этого кода,
если файл test1.txt существует и доступен, а test2.txt нет (1)?
1) test1
2) test1+
3) test1++
4) test2
5) test2+
6) test2++
7) ошибка компиляции
ИСКЛЮЧЕНИЯ И ОШИБКИ
223
Вопрос 8.3.
Дана иерархия исключений:
class A extends java.lang.Exception{}
class B extends A{}
class C extends B{}
class D extends A{}
class E extends A{}
class F extends D{}
class G extends D{}
class H extends E{}
Выберите цепочки блоков catch, использование которых не приведет
к ошибке компиляции, если в соответствующем блоке try могут генерировать-
ся исключения типа C,D,G,H (3):
1) catch(C e){}catch(D e){} catch(H e){}catch(A e){}
2) catch(C e){}catch(D e){}catch(E e){}catch(A e){}
3) catch(C e){}catch(D e){}catch(G e){}catch(A e){}
4) catch(A e){}catch(D e){}catch(G e){}catch(H e){}
5) catch(E e){}catch(D e){}catch(B e){}catch(A e){}
Вопрос 8.4.
Дан код:
class A{
public void f() throws IOException{}
}
class B extends A{}
Каким образом можно переопределить метод f() в классе B, не вызвав при
этом ошибку компиляции (4)?
1) public void f() throws Exception {}
2) public void f() throws IOException {}
3) public void f() throws InterruptedException, IOException {}
4) public void f() throws IOException, FileNotFoundException {}
5) public void f() throws FileNotFoundException {}
6) public void f() throws FileNotFoundException, InternalError {}
ИСПОЛЬЗОВАНИЕ КЛАССОВ И БИБЛИОТЕК
Вопрос 8.5.
Дан код:
public class Quest {
private int qQ;
public Quest(int q) {
qQ = 12 / q;//1
}
public int getQQ() {
return qQ;//2
}
public static void main(String[] args) {
Quest quest = null;
try {
quest = new Quest(0);//3
} catch (Exception e) {//4
}
System.out.println(quest.getQQ());//5
}
}
Укажите строку, выполнение которой приведет к необрабатываемой в дан-
ном коде исключительной ситуации (1):
1) 1
2) 2
3) 3
4) 4
5) 5
225
Достарыңызбен бөлісу: |