Глава 12 Thinking in Java 4th edition |
ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЙ
Один из основополагающих принципов философии Java состоит в том, что «плохо написанная программа не должна запускаться.ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЙ
Один из основополагающих принципов философии Java состоит в том, что «плохо написанная программа не должна запускаться
В идеале ошибки должны обнаруживаться во время компиляции, перед запуском программы. Однако не все ошибки удается выявить в это время. Остальные проблемы приходйтся решать во время работы программы, с помощью механизма, который позволяет источнику ошибки передать необходимую информацию о ней получателю — а последний справляется с возникшими трудностями.
Усовершенствованная система восстановления после ошибок входит в число важнейших факторов, влияющих на надежность кода. Восстановление особенно важно в языке Java, на котором часто пишутся программные компоненты, используемые другими сторонами. Надежная система может быть построена только из надежных компонентов. Унифицированная модель передачи информации об ошибках в Java позволяет компонентам передавать информацию о возникших проблемах в клиентский код.
Механизм исключений значительно упрощает создание больших надежных программ, уменьшает объем необходимого кода и повышает уверенность в том, что в приложении не будет необработанной ошибки. Освоить работу с исключениями несложно, и это. одна из языковых возможностей, способных принести немедленную и значительную выгоду в ваших проектах. В этой главе будет показано, как правильно организовать обработку исключений в программе, а также как сгенерировать собственное исключение, если какой-то из ваших методов сталкивается с непредусмотренными трудностями.
Source(s): Java 4th edition
Основные исключения
Исключительной ситуацией называется проблема, из-за которой нормальное продолжение работы метода или части программы, выполняющихся в данный момент, становится невозможным. Важно отличать исключительную ситуацию от «обычных» ошибок, когда в текущем контексте имеется достаточно информации для преодоления затруднений. В исключительной ситуации обработать исключение в текущем контексте невозможно, потому что вы не располагаете необходимой информацией. Остается единственный выход — покинуть текущий контекст и передать проблему на более высокий уровень. Именно это и происходит при выдаче исключения.
Простейшим примером является деление. Потенциальное деление на нуль может быть выявлено проверкой соответствующего условия. Но что делать, если знаменатель оказался нулем? Возможно, в контексте текущей задачи известно, как следует поступить с нулевым знаменателем. Но, если нулевой знаменатель возник неожиданно, деление в принципе невозможно, и тогда необходимо возбудить исключение, а не продолжать исполнение программы.
При возбуждении исключения происходит сразу несколько вещей. Во-первых, создается объект, представляющий исключение, — точно так же, как и любой другой объект в Java (в куче, оператором new). Далее текущий поток исполнения (тот самый, где произошла ошибка) останавливается, и ссылка на объект, представляющий исключение, извлекается из текущего контекста. С этого момента включается механизм обработки исключений, который начинает поиск подходящего места программы для передачи исключения. Таким местом является обработчик исключений, который пытается решить возникшую проблему так, чтобы программа могла снова попытаться выполнить проблемную операцию или просто продолжила свое выполнение.
В качестве простого примера выдачи исключения представьте ссылку на объект t. Возможно, полученная вами ссылка не была инициализирована; стоит проверить это обстоятельство, прежде чем вызывать методы с использованием этой ссылки. Чтобы передать информацию об ошибке на более высокий уровень, создайте объект, представляющий передаваемую информацию, и «запустите» его из текущего контекста. Тем самым вы возбудите исключение. Вот как это выглядит:
if(t == null) throw new NullPointerException( );
Вырабатывается исключение, которое позволяет вам — в текущем контексте — переложить с себя ответственность, не задумываясь о будущем. Ошибка, словно по волшебству, обрабатывается где-то в другом месте (вскоре мы узнаем, где именно).
Один из основополагающих аспектов исключений состоит в том, что при возникновении нежелательных ситуаций выполнение программы не продолжается по обычному пути. Исключения позволяют вам (в крайнем случае) остановить программу и сообщить о возникших трудностях или (в идеале) разобраться с проблемой и вернуть программу в стабильное состояние.
Source(s): Java 4th edition
Аргументы исключения
Исключения, как и любые объекты Java, создаются в куче оператором new, который выделяет память и вызывает конструктор. У всех стандартных исключений существует два конструктора: стандартный (по умолчанию) и другой, со строковым аргументом, в котором можно разместить подходящую информацию об исключении:
throw new NullPointerException("t = null");
Переданная строка потом может быть извлечена различными способами, о чем будет рассказано позже.Ключевое слово throw влечет за собой ряд довольно интересных действий. Как правило, сначала new используется для создания объекта, представляющего условие происшедшей ошибки. Ссылка на указанный объект передается команде throw. Фактически этот объект «возвращается» методом, несмотря на то что для возвращаемого объекта обычно предусмотрен совсем другой тип. Таким образом, упрощенно можно говорить об обработке исключений как об альтернативном механизме возврата из исполняемого метода (впрочем, с этой аналогией не стоит заходить слишком далеко). Возбуждение исключений также позволяет выходить из простых блоков видимости. В обоих случаях возвращается объект исключения и происходит выход из текущего метода или блока.
Но все сходство с обычным возвратом из метода на этом заканчивается, поскольку при возврате из исключения вы попадаете совсем не туда, куда попали бы при нормальном вызове метода. (Обработчик исключения может находиться очень «далеко» — на расстоянии нескольких уровней в стеке вызова — от метода, где возникла исключительная ситуация.)
Вообще говоря, можно возбудить любой тип исключений, происходящих от объекта Throwable (корневой класс иерархии исключений). Обычно для разных типов ошибок возбуждаются разные типы исключений. Информация о случившейся ошибке как содержится внутри объекта исключения, так и указывается косвенно в самом типе этого объекта, чтобы кто-то на более высоком уровне сумел выяснить, как поступить с исключением. (Нередко именно тип объекта исключения является единственно доступной информацией об ошибке, в то время как внутри объекта никакой полезной информации нет.)
Source(s): Java 4th edition
Перехват исключений
Чтобы увидеть, как перехватываются ошибки, сначала следует усвоить понятие защищенной секции — той части программы, в которой могут произойти исключения и за которой следует специальный блок, отвечающий за обработку этих исключений.
Блок try
Если вы «находитесь» внутри метода и инициируете исключение (или это делает другой вызванный метод), этот метод завершит работу при возникновении исключения. Но если вы не хотите, чтобы оператор throw завершил работу метода, разместите в методе специальный блок для перехвата исключения — так называемый блок try. Этот блок представляет собой простую область действия, которой предшествует ключевое слово
try:
try {
// Фрагмент, способный возбуждать исключения
}
Если бы не обработка исключений, для тщательной проверки ошибок вам пришлось бы добавить к вызову каждого метода дополнительный код для проверки ошибок — даже при многократном вызове одного метода. С обработкой исключений весь код размещается в блоке try, который и перехватывает все возможные исключения в одном месте. А это означает, что вашу программу становится значительно легче писать и читать, поскольку выполняемая задача не смешивается с обработкой ошибок.
Source(s): Java 4th edition
Обработчики исключений
Конечно, возбужденное исключение в конечном итоге должно быть где-то обработано. Этим местом является обработчик исключений, который создается для каждого исключения, которое вы хотите перехватить. Обработчики исключений размещаются прямо за блоком try и обозначаются ключевым словом catch:
try {
// Часть программы, способная возбуждать исключения
} catch(Typel idl) {
// Обработка исключения Typel
} catch(Туре2 id2) {
// Обработка исключения Type2
} catch(ТуреЗ id3) {
// Обработка исключения ТуреЗ
} //И Т.д.
Каждое предложение catch (обработчик исключения) напоминает маленький метод, принимающий один и только один аргумент определенного типа. Идентификатор (idl, id2 и т. д.) может использоваться внутри обработчика точно так же, как и метод распоряжается своими аргументами. Иногда этот идентификатор остается невостребованным, так как тип исключения дает достаточно информации для его обработки, но тем не менее присутствует он всегда.
Обработчики всегда следуют прямо за блоком try. При возникновении исключения механизм обработки исключений ищет первый из обработчиков исключений, аргумент которого соответствует текущему типу исключения. После этого он передает управление в блок catch, и таким образом исключение считается обработанным. После выполнения предложения catch поиск обработчиков исключения прекращается. Выполняется только одна секция catch, соответствующая типу исключения; в этом отношении обработка исключений отличается от команды switch, где нужно дописывать break после каждого case, чтобы предотвратить исполнение всех прочих case.
Заметьте также, что внутри блока try могут вызываться различные методы, способные породить одинаковые типы исключения, но обработчик понадобится всего один.
Прерывание в сравнении с возобновлением
В теории обработки исключений имеется две основные модели. Модель прерывания (которое используется в Java и C++) предполагает, что ошибка настолько серьезна, что при возникновении исключения продолжить исполнение невозможно. Кто бы ни возбудил исключение, сам факт его выдачи означает, что исправить ситуацию «на месте» невозможно и возвращать управление обратно не нужно.
Альтернативная модель называется возобновлением. Она подразумевает, что обработчик ошибок сделает что-то для исправления ситуации, после чего предпринимается попытка повторить неудавшуюся операцию в надежде на успешный исход. В таком случае исключение больше напоминает вызов метода — чтобы применить модель возобновления в Java, вам придется пойти именно по этому пути (то есть не возбуждать исключение, а вызвать метод, способный решить проблему). Также можно создать блок try внутри цикла while, который станет снова и снова обращаться к этому блоку, пока не будет достигнут нужный результат.
Исторически сложилось, что программисты, использующие операционные системы с поддержкой возобновления, со временем переходили к модели прерывания, забывая другую модель. Хотя идея возобновления выглядит привлекательно, она не настолько полезна на практике. Основная причина кроется в обратной связи: обработчик ошибки часто должен знать, где произошло исключение и содержать специальный код для каждого отдельного места ошибки. А это усложняет написание и поддержку программ, особенно для больших систем, где исключения могут быть сгенерированы во многих различных местах.
Создание собственных исключений
Ваш выбор не ограничивается использованием уже существующих в Java исключений. Иерархия исключений JDK не может предусмотреть все возможные ошибки, поэтому вы вправе создавать собственные типы исключений для обозначения специфических ошибок вашей программы.
Для создания собственного класса исключения вам придется определить его производным от уже существующего типа — желательно наиболее близкого к вашей ситуации (хоть это и не всегда возможно). В простейшем случае создается класс с конструктором по умолчанию:
//: exceptions/InheritingExceptions.java
// Создание собственного исключения
class SimpleException extends Exception {}
public class InheritingExceptions {
public void f() throws SimpleException {
System.out.println("Возбуждаем SimpleException из f()");
throw new SimpleException();
}
public static void main(String[] args) {
InheritingExceptions sed = new InheritingExceptions();
try {
sed.f();
} catch(SimpleException e) {
System.out.println("Перехвачено!");
}
}
}
<spoiler text="Output:">
Возбуждаем SimpleExceptі on из f()
Перехвачено!
</spoiler>
Компилятор создает конструктор по умолчанию, который автоматически вызывает конструктор базового класса. Конечно, в этом случае вы лишаетесь конструктора вида SimpleException(String), но на практике он не слишком часто используется. Как вы еще увидите, наиболее важно в исключении именно имя класса, так что в основном исключений, похожих на созданное выше, будет достаточно.
В примере результаты работы выводятся на консоль. Впрочем, их также можно направить в стандартный поток ошибок, что достигается использованием класса System.err. Обычно это правильнее, чем выводить в поток System.out, который может быть перенаправлен. При выводе результатов с помощью System.err пользователь заметит их скорее, чем при выводе в System.out.
Также можно создать класс исключения с конструктором, получающим аргумент String:
//: exceptions/FullConstructors.java
class MyException extends Exception {
public MyException() {}
public MyException(String msg) { super(msg); }
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println("Возбуждаем MyException изf()");
throw new MyException();
}
public static void g() throws MyException {
System.out.println("Возбуждаем MyException из g()");
throw new MyException("Создано в g()");
}
public static void main(String[] args) {
try {
f();
} catch(MyException e) {
e.printStackTrace(System.out);
}
try {
g();
} catch(MyException e) {
e.printStackTrace(System.out);
}
}
}
<spoiler text="Output:">
Возбуждаем MyException из f()
MyException
at Ful1 Constructors.f(Ful1 Constructors.java:11)
at Full Constructors main(FullConstructors.java:19)
Возбуждаем MyException из g()
MyException: Создано в g()
at Full Constructors g(Ful1Constructors.java:15)
at FullConstructors.main(FullConstructors.java 24)
</spoiler>
Изменения незначительны — появилось два конструктора, определяющие способ создания объекта MyException. Во втором конструкторе используется конструктор родительского класса с аргументом String, вызываемый ключевым словом super.
В обработчике исключений вызывается метод printStackTrace() класса Throwable (базового для Exception). Этот метод выводит информацию о последовательности вызовов, которая привела к точке возникновения исключения. В нашем примере информация направляется в System.out, но вызов по умолчанию направляет информацию в стандартный поток ошибок:
e.printStackTrace();
Регистрация исключений
Вспомогательное пространство имен java.util.logging позволяет зарегистрировать информацию об исключениях в журнале. Базовые средства регистрации достаточно просты:
//: exceptions/LoggingExceptions.java
// Регистрация исключений с использованием Logger
import java.util.logging.*;
import java.io.*;
class LoggingException extends Exception {
private static Logger logger =
Logger.getLogger("LoggingException");
public LoggingException() {
StringWriter trace = new StringWriter();
printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
}
public class LoggingExceptions {
public static void main(String[] args) {
try {
throw new LoggingException();
} catch(LoggingException e) {
System.err.println("Caught " + e);
}
try {
throw new LoggingException();
} catch(LoggingException e) {
System.err.println("Caught " + e);
}
}
}
<spoiler text="Output:"> (85% match)
Aug 30, 2005 4:02:31 PM LoggingException <init>
SEVERE: LoggingException
at LoggingExceptions.main(LoggingExceptions.java:19)
Caught LoggingException
Aug 30, 2005 4:02:31 PM LoggingException <init>
SEVERE: LoggingException
at LoggingExceptions.main(LoggingExceptions.java:24)
Caught LoggingException
</spoiler>
Статический метод Logger.getLogger() создает объект Logger, ассоциируемый с аргументом String (обычно имя пакета и класса, к которому относятся ошибки); объект передает свой вывод в System.err. Простейший способ записи информации в Logger заключается в вызове метода, соответствующего уровню ошибки; в нашем примере используется метод severe(). Нам хотелось бы создать String для регистрируемого сообщения из результатов трассировки стека, но метод printStackTrace() по умолчанию не создает String. Для получения String необходимо использовать перегруженную версию printStackTrace() с аргументом java.io.PrintWriter (за подробными объяснениями обращайтесь к главе «Ввод/вывод»). Если передать конструктору PrintWriter объект java.io.StringWriter, для получения вывода в формате String достаточно вызвать toString().
Подход LoggingException чрезвычайно удобен (вся инфраструктура регистрации встроена в само исключение, и все работает автоматически без вмешательства со стороны клиента), однако на практике чаще применяется перехват и регистрация «сторонних» исключений, поэтому сообщение должно генерироваться в обработчике исключения:
//: exceptions/LoggingExceptions2.java
// Регистрация перехваченных исключений
import java.util.logging.*;
import java.io.*;
public class LoggingExceptions2 {
private static Logger logger =
Logger.getLogger("LoggingExceptions2");
static void logException(Exception e) {
StringWriter trace = new StringWriter();
e.printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
public static void main(String[] args) {
try {
throw new NullPointerException();
} catch(NullPointerException e) {
logException(e);
}
}
}
<spoiler text="Output:"> (90% match)
Aug 30, 2005 4:07:54 PM LoggingExceptions2 logException
SEVERE: java.lang.NullPointerException
at LoggingExceptions2.main(LoggingExceptions2.java:16)
</spoiler>
На этом процесс создания собственных исключений не заканчивается — исключение можно снабдить дополнительными конструкторами и элементами:
//: exceptions/ExtraFeatures.java
// Дальнейшее расширение классов исключений
import static net.mindview.util.Print.*;
class MyException2 extends Exception {
private int x;
public MyException2() {}
public MyException2(String msg) { super(msg); }
public MyException2(String msg, int x) {
super(msg);
this.x = x;
}
public int val() { return x; }
public String getMessage() {
return "Detail Message: "+ x + " "+ super.getMessage();
}
}
public class ExtraFeatures {
public static void f() throws MyException2 {
print("Throwing MyException2 from f()");
throw new MyException2();
}
public static void g() throws MyException2 {
print("Throwing MyException2 from g()");
throw new MyException2("Originated in g()");
}
public static void h() throws MyException2 {
print("Throwing MyException2 from h()");
throw new MyException2("Originated in h()", 47);
}
public static void main(String[] args) {
try {
f();
} catch(MyException2 e) {
e.printStackTrace(System.out);
}
try {
g();
} catch(MyException2 e) {
e.printStackTrace(System.out);
}
try {
h();
} catch(MyException2 e) {
e.printStackTrace(System.out);
System.out.println("e.val() = " + e.val());
}
}
}
<spoiler text="Output:">
Throwing MyException2 from f()
MyException2: Detail Message: 0 null
at ExtraFeatures.f(ExtraFeatures.java:22)
at ExtraFeatures.main(ExtraFeatures.java:34)
Throwing MyException2 from g()
MyException2: Detail Message: 0 Originated in g()
at ExtraFeatures.g(ExtraFeatures.java:26)
at ExtraFeatures.main(ExtraFeatures.java:39)
Throwing MyException2 from h()
MyException2: Detail Message: 47 Originated in h()
at ExtraFeatures.h(ExtraFeatures.java:30)
at ExtraFeatures.main(ExtraFeatures.java:44)
e.val() = 47
</spoiler>
Было добавлено поле данных х вместе с методом, считывающим его значение, а также дополнительный конструктор для инициализации х. Переопределенный метод Throwable.getMessage() выводит более содержательную информацию об исключении. Метод getMessage() для классов исключений — аналог toString() в обычных классах.
Так как исключение является просто видом объекта, расширение возможностей классов исключений можно продолжить. Однако следует помнить, что все эти программисты, использующие ваши библиотеки, могут попросту проигнорировать все «украшения» — нередко программисты ограничиваются проверкой типа исключения (как чаще всего бывает со стандартными исключениями Java).
Спецификации исключений
В языке Java желательно сообщать программисту, вызывающему ваш метод, об исключениях, которые данный метод способен возбуждать. Пользователь, вызывающий метод, сможет написать весь необходимый код для перехвата возможных исключений. Конечно, когда доступен исходный код, программист- клиент может пролистать его в поиске предложений throw, но библиотеки не всегда поставляются с исходными текстами. Чтобы библиотека не превращалась в «черный ящик», в Java добавили синтаксис (обязательный для использования), при помощи которого вы сообщаете клиенту об исключениях, возбуждаемых методом, чтобы он сумел правильно обработать их. Этот синтаксис называется спецификацией исключений (exception specification), входит в объявление метода и следует сразу за списком аргументов.
Спецификация исключений состоит из ключевого слова throws, за которым перечисляются все возможные типы исключений. Примерное определение метода будет выглядеть так:
void f() throws TooBig, TooSmall. DivZero { //...
Однако запись
void f() { // ...
означает, что метод не вырабатывает исключений. (Кроме исключений, производных от RuntimeException, которые могут быть возбуждены практически в любом месте — об этом еще будет сказано.)
Обойти спецификацию исключений невозможно — если ваш метод возбуждает исключения и не обрабатывает их, компилятор предложит либо обработать исключение, либо включить его в спецификацию. Жестким контролем за соблюдением правил сверху донизу Java гарантирует правильность использования механизма исключений во время компиляции программы.
Впрочем, «обмануть» компилятор все же можно: вы вправе объявить о возбуждении исключения, которого на самом деле нет. Компилятор верит вам на слово и заставляет пользователей метода поступать так, как будто им и в самом деле необходимо перехватывать исключение. Таким образом можно «зарезервировать» исключение на будущее и уже потом возбуждать его, не изменяя описания готовой программы. Такая возможность может пригодиться и для создания абстрактных базовых классов и интерфейсов, в производных классах которых может возникнуть необходимость в возбуждении исключений.
Исключения, которые проверяются и навязываются еще на этапе компиляции программы, называют контролируемыми (checked).
Перехват произвольных исключений
Можно создать универсальный обработчик, перехватывающий любые типы исключения. Осуществляется это перехватом базового класса всех исключений Exception (существуют и другие базовые типы исключений, но класс Exception является базовым практически для всех программных исключительных ситуаций):
catch(Exception е) {System.out println("nepexвaчeно исключение");}
Подобная конструкция не упустит ни одного исключения, поэтому ее следует размещать в самом конце списка обработчиков, во избежание блокировки следующих за ней обработчиков исключений. Поскольку класс Exception является базовым для всех классов исключений, интересных программисту, сам он не предоставит никакой полезной информации об исключительной ситуации, но можно вызвать методы из его базового типа Throwable:
- String getMessage(), String getLocalizedMessage() возвращают подробное сообщение об ошибке (или сообщение, локализованное для текущего контекста);
- String toString() возвращает короткое описание объекта Throwable, включая подробное сообщение, если оно присутствует;
- void printStackTrace(), void printStackTrace(PrintStream), void printStackTrace(java.io.PrintWriter) выводят информацию об объекте Throwable и трассировку стека вызовов для этого объекта. Трассировка стека вызовов показывает последовательность вызова методов, которая привела к точке возникновения исключения. Первый вариант отправляет информацию в стандартный поток ошибок, второй и третий — в поток по вашему выбору (в главе «Ввод/вывод» вы поймете, почему типов потоков два);
- Throwable fillInStackTrace() записывает в объект Throwable информацию о текущем состоянии стека. Метод используется при повторном возбуждении ошибок или исключений.
Вдобавок в вашем распоряжении находятся методы типа Object, базового для Throwable (и для всех остальных классов). При использовании исключений может пригодиться метод getClass(), который возвращает информацию о классе объекта. Эта информация заключена в объекте типа Class. Например, вы можете узнать имя класса вместе с информацией о пакете методом getName() или получить только имя класса методом getSimpleName().
Рассмотрим пример с использованием основных методов класса Exception:
//: exceptions/ExceptionMethods.java
// Демонстрация методов класса Exception
import static net.mindview.util.Print.*;
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch(Exception e) {
print("Caught Exception");
print("getMessage():" + e.getMessage());
print("getLocalizedMessage():" +
e.getLocalizedMessage());
print("toString():" + e);
print("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
<spoiler text="Output:">
Caught Exception
getMessage():My Exception
getLocalizedMessage():My Exception
toString():java.lang.Exception: My Exception
printStackTrace():
java.lang.Exception: My Exception
at ExceptionMethods.main(ExceptionMethods.java:8)
</spoiler>
Как видите, методы последовательно расширяют объем выдаваемой информации — всякий последующий фактически является продолжением предыдущего.
Трассировка стека
Информацию, предоставляемую методом printStackTrace(), также можно получить напрямую вызовом getStackTrace(). Метод возвращает массив элементов трассировки, каждый из которых представляет один кадр стека. Нулевой элемент представляет вершину стека, то есть последний вызванный метод последовательности (точка, в которой был создан и инициирован объект Throwable).
Соответственно, последний элемент массива представляет «низ» стека, то есть первый вызванный элемент последовательности. Рассмотрим простой пример:
//: exceptions/WhoCalled.java
// Программный доступ к данным трассировки стека
public class WhoCalled {
static void f() {
// Generate an exception to fill in the stack trace
try {
throw new Exception();
} catch (Exception e) {
for(StackTraceElement ste : e.getStackTrace())
System.out.println(ste.getMethodName());
}
}
static void g() { f(); }
static void h() { g(); }
public static void main(String[] args) {
f();
System.out.println("--------------------------------");
g();
System.out.println("--------------------------------");
h();
}
}
<spoiler text="Output:">
f
main
--------------------------------
f
g
main
--------------------------------
f
g
h
main
</spoiler>
Повторное возбуждение исключения
В некоторых ситуациях требуется заново возбудить уже перехваченное исключение; чаще всего это происходит при использовании Exception для перехвата всех исключений. Так как ссылка на текущее исключение уже имеется, вы попросту возбуждаете исключение по этой ссылке:
catch(Exception е) {System.out.println("повторно возбуждено исключение"); throw e;}
При повторном возбуждении исключение передается в распоряжение обработчика более высокого уровня. Все остальные предложения catch текущего блока try игнорируются. Вся информация из объекта, представляющего исключение, сохраняется, и обработчик более высокого уровня, перехватывающий подобные исключения, сможет ее извлечь.
Если вы просто заново возбуждаете исключение, информация о нем, выводимая методом printStackTrace(), будет по-прежнему относиться к месту возникновения исключения, но не к месту его повторного возбуждения. Если вам понадобится использовать новую трассировку стека, вызовите метод fillInStaсkTrасe(), который возвращает исключение (объект Throwable), созданное на базе старого с помещением туда текущей информации о стеке. Вот как это выглядит:
//: exceptions/Rethrowing.java
// // Демонстрация метода fillInStackTrace()
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}
<spoiler text="Output:">
originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
originating the exception in f()
Inside h(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.h(Rethrowing.java:20)
at Rethrowing.main(Rethrowing.java:35)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.h(Rethrowing.java:24)
at Rethrowing.main(Rethrowing.java:35)
</spoiler>
Строка с вызовом fillInStackTrace() становится новой точкой выдачи исключения.
Выдаваемое исключение также может отличаться от исходного. В этом случае эффект получается примерно таким же, как при использовании fillInStackTrace() — информация о месте зарождения исключения теряется, а остается информация, относящаяся к новой команде throw.
//: exceptions/RethrowNew.java
// Повторное возбуждение объекта,
// отличающегося от первоначального
class OneException extends Exception {
public OneException(String s) { super(s); }
}
class TwoException extends Exception {
public TwoException(String s) { super(s); }
}
public class RethrowNew {
public static void f() throws OneException {
System.out.println("originating the exception in f()");
throw new OneException("thrown from f()");
}
public static void main(String[] args) {
try {
try {
f();
} catch(OneException e) {
System.out.println(
"Caught in inner try, e.printStackTrace()");
e.printStackTrace(System.out);
throw new TwoException("from inner try");
}
} catch(TwoException e) {
System.out.println(
"Caught in outer try, e.printStackTrace()");
e.printStackTrace(System.out);
}
}
}
<spoiler text="Output:">
originating the exception in f()
Caught in inner try, e.printStackTrace()
OneException: thrown from f()
at RethrowNew.f(RethrowNew.java:15)
at RethrowNew.main(RethrowNew.java:20)
Caught in outer try, e.printStackTrace()
TwoException: from inner try
at RethrowNew.main(RethrowNew.java:25)
</spoiler>
О последнем исключении известно только то, что оно поступило из внутреннего блока try, но не из метода f().
Вам никогда не придется заботиться об удалении предыдущих исключений, и исключений вообще. Все они являются объектами, созданными в общей куче оператором new, и сборщик мусора уничтожает их автоматически.
Цепочки исключений
Зачастую необходимо перехватить одно исключение и возбудить следующее, не потеряв при этом информации о первом исключении — это называется цепочкой исключений (exception chaining). До выпуска пакета JDK 1.4 программистам приходилось самостоятельно писать код, сохраняющий информацию о предыдущем исключении, однако теперь конструкторам всех подклассов Throwable может передаваться объект-причина (cause). Предполагается, что причиной является изначальное исключение и передача ее в новый объект обеспечивает трассировку стека вплоть до самого его начала, хотя при этом создается и возбуждается новое исключение.
Интересно отметить, что единственными подклассами класса Throwable, принимающими объект-причину в качестве аргумента конструктора, являются три основополагающих класса исключений: Error (используется виртуальной машиной (JVM) для сообщений о системных ошибках), Exception и RuntimeException. Для организации цепочек из других типов исключений придется использовать метод initCause(), а не конструктор.
Следующий пример демонстрирует динамическое добавление полей в объект DynamicFields во время работы программы:
//: exceptions/DynamicFields.java
// Динамическое добавление полей в класс.
// Пример использования цепочки исключений
import static net.mindview.util.Print.*;
class DynamicFieldsException extends Exception {}
public class DynamicFields {
private Object[][] fields;
public DynamicFields(int initialSize) {
fields = new Object[initialSize][2];
for(int i = 0; i < initialSize; i++)
fields[i] = new Object[] { null, null };
}
public String toString() {
StringBuilder result = new StringBuilder();
for(Object[] obj : fields) {
result.append(obj[0]);
result.append(": ");
result.append(obj[1]);
result.append("\n");
}
return result.toString();
}
private int hasField(String id) {
for(int i = 0; i < fields.length; i++)
if(id.equals(fields[i][0]))
return i;
return -1;
}
private int
getFieldNumber(String id) throws NoSuchFieldException {
int fieldNum = hasField(id);
if(fieldNum == -1)
throw new NoSuchFieldException();
return fieldNum;
}
private int makeField(String id) {
for(int i = 0; i < fields.length; i++)
if(fields[i][0] == null) {
fields[i][0] = id;
return i;
}
// No empty fields. Add one:
Object[][] tmp = new Object[fields.length + 1][2];
for(int i = 0; i < fields.length; i++)
tmp[i] = fields[i];
for(int i = fields.length; i < tmp.length; i++)
tmp[i] = new Object[] { null, null };
fields = tmp;
// Recursive call with expanded fields:
return makeField(id);
}
public Object
getField(String id) throws NoSuchFieldException {
return fields[getFieldNumber(id)][1];
}
public Object setField(String id, Object value)
throws DynamicFieldsException {
if(value == null) {
// Most exceptions don't have a "cause" constructor.
// In these cases you must use initCause(),
// available in all Throwable subclasses.
DynamicFieldsException dfe =
new DynamicFieldsException();
dfe.initCause(new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if(fieldNumber == -1)
fieldNumber = makeField(id);
Object result = null;
try {
result = getField(id); // Get old value
} catch(NoSuchFieldException e) {
// Use constructor that takes "cause":
throw new RuntimeException(e);
}
fields[fieldNumber][1] = value;
return result;
}
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
print(df);
try {
df.setField("d", "A value for d");
df.setField("number", 47);
df.setField("number2", 48);
print(df);
df.setField("d", "A new value for d");
df.setField("number3", 11);
print("df: " + df);
print("df.getField(\"d\") : " + df.getField("d"));
Object field = df.setField("d", null); // Exception
} catch(NoSuchFieldException e) {
e.printStackTrace(System.out);
} catch(DynamicFieldsException e) {
e.printStackTrace(System.out);
}
}
}
<spoiler text="Output:">
null: null
null: null
null: null
d: A value for d
number: 47
number2: 48
df: d: A new value for d
number: 47
number2: 48
number3: 11
df.getField("d") : A new value for d
DynamicFieldsException
at DynamicFields.setField(DynamicFields.java:64)
at DynamicFields.main(DynamicFields.java:94)
Caused by: java.lang.NullPointerException
at DynamicFields.setField(DynamicFields.java:66)
... 1 more
</spoiler>
Каждый объект DynamicFields содержит массив пар Object-Object. Первый объект содержит идентификатор поля (String), а второй объект — значение поля, которое может быть любого типа, кроме неупакованных примитивов. При создании объекта необходимо оценить примерное количество полей. Метод setField() либо находит уже существующее поле с заданным именем, либо создает новое поле и сохраняет значение. Когда пространство для полей заканчивается, метод наращивает его, создавая массив размером на единицу больше и копируя в него старые элементы. При попытке размещения пустой ссылки null метод инициирует исключение DynamicFieldsException, создавая объект нужного типа и передавая методу initCause() в качестве причины исключение NullPointerException.
Для возвращаемого значения метод setField() использует старое значение поля, получая его методом getField(), который может возбудить исключение NoSuchFieldException. Если метод getField() вызывает программист-клиент, то он ответственен за обработку возможного исключения NoSuchFieldException, однако, если последнее возникает в методе setField(), это является ошибкой программы; соответственно, полученное исключение преобразуется в исключение RuntimeException с помощью конструктора, принимающего аргумент-причину.
Для создания результата toString() использует объект StringBuilder. Этот класс будет подробно рассмотрен при описании работы со строками.
Стандартные исключения Java
Класс Java Throwable описывает все объекты, которые могут возбуждаться как исключения. Существует две основные разновидности объектов Throwable (то есть ветви наследования). Тип Error представляет системные ошибки и ошибки времени компиляции, которые обычно не перехватываются (кроме нескольких особых случаев). Тип Exception может быть возбужден из любого метода стандартной библиотеки классов Java или пользовательского метода в случае неполадок при исполнении программы. Таким образом, для программистов интерес представляет прежде всего тип Exception.
Лучший способ получить представление об исключениях — просмотреть документацию JDK. Стоит сделать это хотя бы раз, чтобы получить представление о различных видах исключений, но вскоре вы убедитесь в том, что наиболее принципиальным различием между разными исключениями являются их имена. К тому же количество исключений в Java постоянно растет, и едва ли имеет смысл описывать их в книге. Любая программная библиотека от стороннего производителя, скорее всего, также будет иметь собственный набор исключений. Здесь важнее понять принцип работы и поступать с исключениями сообразно.
Основной принцип заключается в том, что имя исключения относительно полно объясняет суть возникшей проблемы. Не все исключения определены в пакете java.lang, некоторые из них созданы для поддержки других библиотек, таких как util, net и io, как можно видеть из полных имен их классов или из базовых классов. Например, все исключения, связанные с вводом/выводом (I/O), унаследованы от java.io.IOException.
Особый случай: RuntimeException
Вспомним первый пример в этой главе:
if(t == null) throw new NullPointerException();
Только представьте, как ужасно было бы проверять таким образом каждую ссылку, переданную вашему методу. К счастью, делать это не нужно — такая проверка автоматически выполняется во время исполнения Java-программы, и при попытке использования null-ссылок автоматически возбуждается NullPointerException. Таким образом, использованная в примере конструкция избыточна.
Есть целая группа исключений, принадлежащих к этой категории. Они всегда возбуждаются в Java автоматически, и вам не придется включать их в спецификацию исключений. Все они унаследованы от одного базового класса RuntimeException, что дает нам идеальный пример наследования: семейство классов, имеющих общие характеристики и поведение. Вам также не придется создавать спецификацию исключений, указывающую на возбуждение методом RuntimeException (или любого унаследованного от него исключения), так как эти исключения относятся к неконтролируемым (unchecked). Такие исключения означают ошибки в программе, и фактически вам никогда не придется перехватывать их — это делается автоматически. Проверка RuntimeException привела бы к излишнему загромождению программы. И хотя вам обычно не требуется перехватывать RuntimeException, возможно, вы посчитаете нужным возбуждать некоторые из них в своих собственных библиотеках программ.
Что же происходит, когда подобные исключения не перехватываются? Так как компилятор не заставляет перечислять такие исключения в спецификациях, можно предположить, что исключение RuntimeException проникнет прямо в метод main(), и не будет перехвачено. Чтобы увидеть все в действии, испытайте следующий пример:
//: exceptions/NeverCaught.java
// Игнорирование RuntimeExceptions.
// {ThrowsException}
public class NeverCaught {
static void f() {
throw new RuntimeException("From f()");
}
static void g() {
f();
}
public static void main(String[] args) {
g();
}
}
Можно сразу заметить, что RuntimeException (и все от него унаследованное) является специальным случаем, так как компилятор не требует для него спецификации исключения. Выходные данные выводятся в System.err :
- Exception in thread "main" java.lang.RuntimeException- Из f() at NeverCaught.f(NeverCaught.java:7) at NeverCaught.g(NeverCaught.java:10) at NeverCaught.main(NeverCaught.java-13)
Мы приходим к ответу на поставленный вопрос: если RuntimeException добирается до main() без перехвата, то работа программы завершается с вызовом метода printStackTrace().
Помните, что только исключения типа RuntimeException (и производных классов) могут игнорироваться во время написания текста программы, в то время как остальные действия компилятор осуществляет в обязательном порядке. Это объясняется тем, что RuntimeException является следствием ошибки программиста, например:
- ошибки, которую невозможно предвидеть (к примеру, получение null- ссылки в вашем методе, переданной снаружи);
- ошибки, которую вы как программист должны были проверить в вашей программе (подобной ArraylndexOutOfBoundsException, с проверкой размера массива). Ошибки первого вида часто становятся причиной ошибок второго вида.
В подобных ситуациях исключения оказывают неоценимую помощь в отладочном процессе.
Назвать механизм исключений Java узкоспециализированным инструментом было бы неверно. Да, он помогает справиться с досадными ошибками на стадии исполнения программы, которые невозможно предусмотреть заранее, но при этом данный механизм помогает выявлять многие ошибки программирования, выходящие за пределы возможностей компилятора.
Завершение с помощью finally
Часто встречается ситуация, когда некоторая часть программы должна выполняться независимо от того, было или нет возбуждено исключение внутри блока try. Обычно это имеет отношение к операциям, не связанным с освобождением памяти (так как это входит в обязанности сборщика мусора). Для достижения желаемой цели необходимо разместить блок finally после всех обработчиков исключений. Таким образом, полная конструкция обработки исключения выглядит так:
try {
// Защищенная секция: рискованные операции,
// которые могут породить исключения А, В. или С
} catch(A al) {
// Обработчик для ситуации А
} catch(B bl) {
// Обработчик для ситуации В
} catch(C cl) {
// Обработчик для ситуации С
} finally {
// Действия, производимые в любом случае
}
Чтобы продемонстрировать, что блок finally выполняется всегда, рассмотрим следующую программу:
//: exceptions/FinallyWorks.java
// Блок finally выполняется всегда
class ThreeException extends Exception {}
public class FinallyWorks {
static int count = 0;
public static void main(String[] args) {
while(true) {
try {
// Post-increment is zero first time:
if(count++ == 0)
throw new ThreeException();
System.out.println("No exception");
} catch(ThreeException e) {
System.out.println("ThreeException");
} finally {
System.out.println("In finally clause");
if(count == 2) break; // out of "while"
}
}
}
}
<spoiler text="Output:">
ThreeException
In finally clause
No exception
In finally clause
</spoiler>
Результат работы программы показывает, что вне зависимости от того, было ли возбуждено исключение, предложение finally выполняется всегда.
Данный пример также подсказывает, как справиться с тем фактом, что Java не позволяет вернуться к месту возникновения исключения, о чем говорилось ранее. Если расположить блок try в цикле, можно также определить условие, на основании которого будет решено, должна ли программа продолжаться. Также можно добавить статический счетчик или иной механизм для проверки нескольких разных решений, прежде чем отказаться от попыток восстановления. Это один из способов обеспечения повышенной отказоустойчивости программ.
Для чего нужен блок finally?
В языках без сборки мусора и без автоматических вызовов деструкторов блок finally гарантирует освобождение ресурсов и памяти независимо от того, что случилось в блоке try. В Java существует сборщик мусора, поэтому с освобождением памяти проблем не бывает. Также нет необходимости вызывать деструкторы, их просто нет. Когда же нужно использовать finally в Java?
Блок finally необходим тогда, когда в исходное состояние вам необходимо вернуть что-то другое, а не память. Это может быть, например, открытый файл или сетевое подключение, часть изображения на экране или даже какой-то физический переключатель, вроде смоделированного в следующем примере:
//: exceptions/Switch.java
import static net.mindview.util.Print.*;
public class Switch {
private boolean state = false;
public boolean read() { return state; }
public void on() { state = true; print(this); }
public void off() { state = false; print(this); }
public String toString() { return state ? "on" : "off"; }
}
//: exceptions/OnOffException1.java
public class OnOffException1 extends Exception {}
//: exceptions/OnOffException2.java
public class OnOffException2 extends Exception {}
//: exceptions/OnOffSwitch.java
// Зачем использовать finally?
public class OnOffSwitch {
private static Switch sw = new Switch();
public static void f()
throws OnOffException1,OnOffException2 {}
public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
f();
sw.off();
} catch(OnOffException1 e) {
System.out.println("OnOffException1");
sw.off();
} catch(OnOffException2 e) {
System.out.println("OnOffException2");
sw.off();
}
}
}
<spoiler text="Output:">
on
off
</spoiler>
Наша цель — убедиться в том, что переключатель был выключен по завершении метода main(), поэтому в конце блока try и в конце каждого обработчика исключения помещается вызов sw.off(). Однако в программе может возникнуть неперехватываемое исключение, и тогда вызов sw.off() будет пропущен. Однако благодаря finally завершающий код можно поместить в одном определенном месте:
//: exceptions/WithFinally.java
// Finally гарантирует выполнение завершающего кода.
public class WithFinally {
static Switch sw = new Switch();
public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
OnOffSwitch.f();
} catch(OnOffException1 e) {
System.out.println("OnOffException1");
} catch(OnOffException2 e) {
System.out.println("OnOffException2");
} finally {
sw.off();
}
}
}
<spoiler text="Output:">
on
off
</spoiler>
Здесь вызов метода sw.off() просто перемещен в то место, где он гарантированно будет выполнен.
Даже если исключение не перехватывается в текущем наборе условий catch, блок finally отработает перед тем, как механизм обработки исключений продолжит поиск обработчика на более высоком уровне:
//: exceptions/AlwaysFinally.java
// Finally выполняется всегда
import static net.mindview.util.Print.*;
class FourException extends Exception {}
public class AlwaysFinally {
public static void main(String[] args) {
print("Entering first try block");
try {
print("Entering second try block");
try {
throw new FourException();
} finally {
print("finally in 2nd try block");
}
} catch(FourException e) {
System.out.println(
"Caught FourException in 1st try block");
} finally {
System.out.println("finally in 1st try block");
}
}
}
<spoiler text="Output:">
Entering first try block
Entering second try block
finally in 2nd try block
Caught FourException in 1st try block
finally in 1st try block
</spoiler>
Блок finally также исполняется при использовании команд break и continue. Заметьте, что комбинация finally в сочетании с break и continue с метками снимает в Java всякую необходимость в операторе goto.
Использование finally с return
Поскольку секция finally выполняется всегда, важные завершающие действия будут выполнены даже при возврате из нескольких точек метода:
//: exceptions/MultipleReturns.java
import static net.mindview.util.Print.*;
public class MultipleReturns {
public static void f(int i) {
print("Initialization that requires cleanup");
try {
print("Point 1");
if(i == 1) return;
print("Point 2");
if(i == 2) return;
print("Point 3");
if(i == 3) return;
print("End");
return;
} finally {
print("Performing cleanup");
}
}
public static void main(String[] args) {
for#40;int i = 1; i <= 4; i++)
f(i);
}
}
<spoiler text="Output:">
Initialization that requires cleanup
Point 1
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
End
Performing cleanup
</spoiler>
Из выходных данных видно, что выполнение finally не зависит от того, в какой точке защищенной секции была выполнена команда return.
Проблема потерянных исключений
К сожалению, реализация механизма исключений в Java не обошлась без изъяна. Хотя исключение сигнализирует об аварийной ситуации в программе и никогда не должно игнорироваться, оно может быть потеряно. Это происходит при использовании finally в конструкции определенного вида:
//: exceptions/LostMessage.java
// Как теряются исключения.
class VeryImportantException extends Exception {
public String toString() {
return "A very important exception!";
}
}
class HoHumException extends Exception {
public String toString() {
return "A trivial exception";
}
}
public class LostMessage {
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args) {
try {
LostMessage lm = new LostMessage();
try {
lm.f();
} finally {
lm.dispose();
}
} catch(Exception e) {
System.out.println(e);
}
}
}
<spoiler text="Output:">
A trivial exception
</spoiler>
В выводе нет никаких признаков VeryImportantException, оно было просто замещено исключением HoHumException в предложении finally. Это очень серьезный недочет, так как потеря исключения может произойти в гораздо более скрытой и трудно диагностируемой ситуации, в отличие от той, что показана в примере. Например, в C++ подобная ситуация (возбуждение второго исключения без обработки первого) рассматривается как грубая ошибка программиста. Возможно, в новых версиях Java эта проблема будет решена (впрочем, любой метод, способный возбуждать исключения — такой, как dispose() в приведенном примере — обычно заключается в конструкцию try-catch).
Еще проще потерять исключение простым возвратом из finally:
//: exceptions/ExceptionSilencer.java
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
// Команда 'return' в блоке finally
// прерывает обработку исключения
return;
}
}
}
Запустив эту программу, вы увидите, что она ничего не выводит — несмотря на исключение.
Ограничения при использовании исключений
В переопределенном методе можно возбуждать только те исключения, которые были описаны в методе базового класса. Это полезное ограничение означает, что программа, работающая с базовым классом, автоматически сможет работать и с объектом, произошедшим от базового (конечно, это фундаментальный принцип ООП), включая и исключения.
Следующий пример демонстрирует виды ограничений (во время компиляции), наложенные на исключения:
//: exceptions/StormyInning.java
// Переопределенные методы могут возбуждать только
// исключения, описанные в версии базового класса,
// или исключения, унаследованные от исключений
// базового класса.
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
abstract class Inning {
public Inning() throws BaseballException {}
public void event() throws BaseballException {
// Doesn't actually have to throw anything
}
public abstract void atBat() throws Strike, Foul;
public void walk() {} // Throws no checked exceptions
}
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StormyInning extends Inning implements Storm {
// OK to add new exceptions for constructors, but you
// must deal with the base constructor exceptions:
public StormyInning()
throws RainedOut, BaseballException {}
public StormyInning(String s)
throws Foul, BaseballException {}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
//! public void event() throws RainedOut {}
// If the method doesn't already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {}
// You can choose to not throw any exceptions,
// even if the base version does:
public void event() {}
// Overridden methods can throw inherited exceptions:
public void atBat() throws PopFoul {}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch(PopFoul e) {
System.out.println("Pop foul");
} catch(RainedOut e) {
System.out.println("Rained out");
} catch(BaseballException e) {
System.out.println("Generic baseball exception");
}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch(Strike e) {
System.out.println("Strike");
} catch(Foul e) {
System.out.println("Foul");
} catch(RainedOut e) {
System.out.println("Rained out");
} catch(BaseballException e) {
System.out.println("Generic baseball exception");
}
}
}
В классе Inning и конструктор, и метод event() объявляют, что будут возбуждать исключения, но в действительности этого не делают. Это допустимо, поскольку подобный подход заставляет пользователя перехватывать все виды исключений, которые потом могут быть добавлены в переопределенные версии метода event(). Данный принцип распространяется и на абстрактные методы, что и показано для метода atBat().
Интерфейс Storm интересен тем, что содержит один метод (event()), уже определенный в классе Inning, и один уникальный. Оба метода возбуждают новый тип исключения RainedOut. Когда класс StormyInning расширяет Inning и реализует интерфейс Storm, выясняется, что метод event() из Storm не способен изменить тип исключения для метода event() класса Inning. Опять-таки это вполне разумно, так как иначе вы бы никогда не знали, перехватываете ли нужное исключение в случае работы с базовым классом. Конечно, когда метод, описанный в интерфейсе, отсутствует в базовом классе (как rainHard()), никаких проблем с возбуждением исключений нет.
Метод StormyInning.walk() не компилируется из-за того, что он возбуждает исключение, тогда как Inning.walk() такого не делает. Если бы это позволялось, вы могли бы написать код, вызывающий метод Inning.walk() и не перехватывающий никаких исключений, а потом при подстановке объекта класса, производного от Inning, возникли бы исключения, нарушающие работу программы. Таким образом, принудительно обеспечивая соответствие спецификаций исключений в производных и базовых версиях методов, Java добивается взаимозаменяемости объектов.
Переопределенный метод event() показывает, что метод производного класса может вообще не возбуждать исключений, даже если это делается в базовой версии. Опять-таки это нормально, так как не влияет на уже написанный код — подразумевается, что метод базового класса возбуждает исключения. Аналогичная логика применима для метода atBat(), возбуждающего исключение PopFoul, производное от Foul, которое возбуждается базовой версией atBat(). Итак, если вы пишете код, работающий с Inning и вызывающий atBat(), то он должен перехватывать исключение Foul. Так как PopFoul наследует от Foul, обработчик исключения для Foul перехватит и PopFoul.
Последний интересный момент встречается в методе main(). Мы видим, что при работе именно с объектом StormyInning компилятор заставляет перехватывать только те исключения, которые характерны для этого класса, но при восходящем преобразовании к базовому типу компилятор заставляет перехватывать исключения из базового класса. Все эти ограничения значительно повышают ясность и надежность кода обработки исключений.
Хотя компилятор заставляет описывать исключения при наследовании, спецификация исключений не является частью объявления (сигнатуры) метода, которое состоит только из имени метода и типов аргументов. Соответственно, нельзя переопределять методы только по спецификациям исключений. Вдобавок, даже если спецификация исключения присутствует в методе базового класса, это вовсе не гарантирует его существования в методе производного класса. Данная практика сильно отличается от правил наследования, по которым метод базового класса обязательно присутствует и в производном классе. Другими словами, «интерфейс спецификации исключений» для определенного метода может сузиться в процессе наследования и переопределения, но никак не расшириться — и это прямая противоположность интерфейсу класса во время наследования.
Конструкторы
При программировании обработки исключений всегда спрашивайте себя: «Если произойдет исключение, будет ли все корректно завершено?» Чаще все идет более или менее безопасно, но с конструкторами возникает проблема. Конструктор приводит объект в определенное начальное состояние, но может начать выполнять какое-либо действие — такое как открытие файла — которое не будет правильно завершено, пока пользователь не освободит объект, вызвав специальный завершающий метод. Если исключение произойдет в конструкторе, эти финальные действия могут быть исполнены ошибочно. А это означает, что при написании конструкторов необходимо быть особенно внимательным.
Казалось бы, блок finally решает все проблемы. Но в действительности все сложнее — ведь finally выполняется всегда, и даже тогда, когда завершающий код не должен активизироваться до вызова какого-то метода. Если сбой в конструкторе произойдет где-то на середине, может оказаться, что часть объекта, освобождаемая в finally, еще не была создана.
В следующем примере создается класс, названный InputFile, который открывает файл и позволяет читать из него по одной строке. Он использует классы FileReader и BufferedReader из стандартной библиотеки ввода/вывода Java, которая будет изучена далее, но эти классы достаточно просты, и у вас не возникнет особых сложностей при работе с ними:
//: exceptions/InputFile.java
// Специфика исключений в конструкторах
import java.io.*;
public class InputFile {
private BufferedReader in;
public InputFile(String fname) throws Exception {
try {
in = new BufferedReader(new FileReader(fname));
// Other code that might throw exceptions
} catch(FileNotFoundException e) {
System.out.println("Could not open " + fname);
// Wasn't open, so don't close it
throw e;
} catch(Exception e) {
// All other exceptions must close it
try {
in.close();
} catch(IOException e2) {
System.out.println("in.close() unsuccessful");
}
throw e; // Rethrow
} finally {
// Don't close it here!!!
}
}
public String getLine() {
String s;
try {
s = in.readLine();
} catch(IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}
public void dispose() {
try {
in.close();
System.out.println("dispose() successful");
} catch(IOException e2) {
throw new RuntimeException("in.close() failed");
}
}
}
Конструктор InputFile получает в качестве аргумента строку (String) с именем открываемого файла. Внутри блока try он создает объект FileReader для этого файла. Класс FileReader не особенно полезен сам по себе, поэтому мы встраиваем его в созданный BufferedReader, с которым и работаем, — одно из преимуществ InputFile состоит в том, что он объединяет эти два действия.
Если при вызове конструктора FileReader произойдет сбой, возбуждается исключение FileNotFoundException. В этом случае закрывать файл не нужно, так как он и не открывался. Все остальные блоки catch обязаны закрыть файл, так как он уже был открыт во время входа в них. (Конечно, все было бы сложнее в случае, если бы несколько методов могли возбуждать FileNotFoundException. В таких ситуациях обычно требуется несколько блоков try.) Метод close() тоже может возбудить исключение, которое также проверяется и перехватывается — несмотря на то, что вызов находится в другом блоке catch — с точки зрения компилятора Java это всего лишь еще одна пара фигурных скобок. После выполнения всех необходимых локальных действий исключение возбуждается заново; ведь вызывающий метод не должен считать, что объект был благополучно создан.
В этом примере блок finally определенно не подходит для закрытия файла, поскольку в таком варианте закрытие происходило бы каждый раз по завершении работы конструктора. Мы хотим, чтобы файл оставался открытым на протяжении всего жизненного цикла InputFile.
Метод getLine() возвращает объект String со следующей строкой из файла. Он вызывает метод readLine(), способный возбуждать исключения, но они перехватываются; таким образом, сам getLine() исключений не возбуждает. При проектировании обработки исключений вы выбираете между полной обработкой исключения на определенном уровне, его частичной обработкой и передачей далее того же (или другого) исключения и, наконец, простой передачей далее. Там, где это возможно, передача исключения значительно упрощает программирование. В данной ситуации метод getLine() преобразует исключение в RuntimeException, чтобы указать на ошибку в программе.
Метод dispose() должен вызываться пользователем при завершении работы с объектом InputFile. Он освобождает системные ресурсы (такие, как открытые файлы), закрепленные за объектами BufferedReader и (или) FileReader. Делать это следует только тогда, когда работа с объектом InputFile действительно будет завершена. Казалось бы, подобные действия удобно разместить в методе finalize(), но, как упоминалось в главе 5, вызов этого метода не гарантирован (и даже если вы знаете, что он будет вызван, то неизвестно, когда). Это один из недостатков Java: все завершающие действия, кроме освобождения памяти, не производятся автоматически, так что вам придется информировать пользователя о том, что он ответственен за их выполнение.
Самый безопасный способ использования класса, который способен выдать исключение при конструировании и требует завершающих действий, основан на использовании вложенных блоков try:
//: exceptions/Cleanup.java
// Гарантированное освобождение ресурсов.
public class Cleanup {
public static void main(String[] args) {
try {
InputFile in = new InputFile("Cleanup.java");
try {
String s;
int i = 1;
while((s = in.getLine()) != null)
; // Perform line-by-line processing here...
} catch(Exception e) {
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
} finally {
in.dispose();
}
} catch(Exception e) {
System.out.println("InputFile construction failed");
}
}
}
<spoiler text="Output:">
dispose() successful
</spoiler>
Присмотритесь к логике происходящего: конструирование объекта InputFile фактически заключено в собственный блок try. Если попытка завершается неудачей, мы входим во внешнюю секцию catch и метод dispose() не вызывается. Но, если конструирование прошло успешно, мы хотим обеспечить гарантированное завершение, поэтому сразу же после конструирования создается новый блок try. Блок finally, выполняющий завершение, связывается с внутренним блоком try; таким образом, блок finally не выполняется при неудачном конструировании и всегда выполняется, если конструирование прошло удачно.
Эта универсальная идиома применяется и в тех ситуациях, когда конструктор не выдает исключений. Основной принцип: сразу же после создания объекта, требующего завершения, начинается конструкция try-finally:
//: exceptions/CleanupIdiom.java
// За каждым освобождаемым объектом следует try-finally
class NeedsCleanup { // Construction can't fail
private static long counter = 1;
private final long id = counter++;
public void dispose() {
System.out.println("NeedsCleanup " + id + " disposed");
}
}
class ConstructionException extends Exception {}
class NeedsCleanup2 extends NeedsCleanup {
// Construction can fail:
public NeedsCleanup2() throws ConstructionException {}
}
public class CleanupIdiom {
public static void main(String[] args) {
// Section 1:
NeedsCleanup nc1 = new NeedsCleanup();
try {
// ...
} finally {
nc1.dispose();
}
// Section 2:
// If construction cannot fail you can group objects:
NeedsCleanup nc2 = new NeedsCleanup();
NeedsCleanup nc3 = new NeedsCleanup();
try {
// ...
} finally {
nc3.dispose(); // Reverse order of construction
nc2.dispose();
}
// Section 3:
// If construction can fail you must guard each one:
try {
NeedsCleanup2 nc4 = new NeedsCleanup2();
try {
NeedsCleanup2 nc5 = new NeedsCleanup2();
try {
// ...
} finally {
nc5.dispose();
}
} catch(ConstructionException e) { // nc5 constructor
System.out.println(e);
} finally {
nc4.dispose();
}
} catch(ConstructionException e) { // nc4 constructor
System.out.println(e);
}
}
<spoiler text="Output:">
NeedsCleanup 1 disposed
NeedsCleanup 3 disposed
NeedsCleanup 2 disposed
NeedsCleanup 5 disposed
NeedsCleanup 4 disposed
</spoiler>
Секция 1 метода main() весьма прямолинейна: за созданием завершаемого объекта следует try-finally. Если конструирование не может завершиться неудачей, наличие catch не требуется. В секции 2 мы видим, что конструкторы, которые не могут завершиться неудачей, могут группироваться как для конструирования, так и для завершения.
Секция 3 показывает, как поступать с объектами, при конструировании которых возможны сбои и которые нуждаются в завершении. Здесь программа усложняется, потому что каждое конструирование должно заключаться в отдельную копию try-catch и за ним должна следовать конструкция try-finally, обеспечивающая завершение.
Неудобства обработки исключения в подобных случаях — веский аргумент в пользу создания конструкторов, выполнение которых заведомо обходится без сбоев (хотя это и не всегда возможно).
Идентификация исключений
Механизм обработки исключений ищет в списке «ближайший» подходящий обработчик в порядке их следования. Когда соответствие обнаруживается, исключение считается найденным и дальнейшего поиска не происходит.
Идентификация исключений не требует обязательного соответствия между исключением и обработчиком. Объект порожденного класса подойдет и для обработчика, изначально написанного для базового класса:
//: exceptions/Human.java
// Перехват иерархии исключений.
class Annoyance extends Exception {}
class Sneeze extends Annoyance {}
public class Human {
public static void main(String[] args) {
// Catch the exact type:
try {
throw new Sneeze();
} catch(Sneeze s) {
System.out.println("Caught Sneeze");
} catch(Annoyance a) {
System.out.println("Caught Annoyance");
}
// Catch the base type:
try {
throw new Sneeze();
} catch(Annoyance a) {
System.out.println("Caught Annoyance");
}
}
}
<spoiler text="Output:">
Caught Sneeze
Caught Annoyance
</spoiler>
Исключение Sneeze будет перехвачено в первом блоке catch, который ему соответствует — конечно, это будет первый блок. Но, если удалить первый блок catch, оставив только проверку Annoyance, программа все равно работает, потому что она перехватывает базовый класс Sneeze. Другими словами, блок catch (Annoyance а) поймает Annoyance или любой другой класс, унаследованный от него. Если вы добавите новые производные исключения в свой метод, программа пользователя этого метода не потребует изменений, так как клиент перехватывает исключения базового класса.
Если вы попытаетесь «замаскировать» исключения производного класса, поместив сначала блок catch базового класса:
try { throw new Sneeze();
} catch(Annoyance a) {
// ..
} catch(Sneeze s) {
//...
}
компилятор выдаст сообщение об ошибке, так как он видит, что блок catch для исключения Sneeze никогда не выполнится.
Альтернативные решения
Система обработки исключений представляет собой «черный ход», позволяющий программе нарушить нормальную последовательность выполнения команд. «Черный ход» открывается при возникновении «исключительных ситуаций», когда обычная работа далее невозможна или нежелательна. Исключения представляют собой условия, с которыми текущий метод справиться не в состоянии. Причина, по которой возникают системы обработки исключений, кроется в том, что программисты не желали иметь дела с громоздкой проверкой всех возможных условий возникновения ошибок каждой функции. В результате ошибки ими просто игнорировались. Стоит отметить, что вопрос удобства программиста при обработке ошибок стоял на первом месте для разработчиков Java.
Основное правило при использовании исключений гласит: «Не обрабатывайте исключение, если вы не знаете, что с ним делать». По сути, отделение кода, ответственного за обработку ошибок, от места, где ошибка возникает, является одной из главных целей обработки исключений. Это позволяет вам сконцентрироваться на том, что вы хотите сделать в одном фрагменте кода, и на том, как вы собираетесь поступить с ошибками в совершенно другом месте программы. В результате основной код не перемежается с логикой обработки ошибок, что упрощает его сопровождение и понимание. Исключения также сокращают объем кода, так как один обработчик может обслуживать несколько потенциальных источников ошибок.
Контролируемые исключения немного усложняют ситуацию, поскольку они заставляют добавлять обрабатывающие исключения предложения там, где вы не всегда еще готовы справиться с ошибкой. В итоге возникает проблема «проглоченных исключений»:
try {
// делает что-то полезное
} саtch (ОбязывающееИсключение е) {} // Проглотили!
Программисты (и я в том числе, в первом издании книги), не долго думая, делали самое бросающееся в глаза и «проглатывали» исключение — зачастую непреднамеренно, но, как только дело было сделано, компилятор был удовлетворен, поэтому пока вы не вспоминали о необходимости пересмотреть и исправить код, не вспоминали и об исключении. Исключение происходит, но безвозвратно теряется. Из-за того что компилятор заставляет вас писать код для обработки исключений прямо на месте, это кажется самым простым решением, хотя на самом деле ничего хуже и придумать нельзя.
Ужаснувшись тем, что я так поступил, во втором издании книги я «исправил» проблему, распечатыв в обработчике трассировку стека исключения (и сейчас это можно видеть — в подходящих местах — в некоторых примерах данной главы). Хотя это и полезно при отслеживании поведения исключений, трассировка фактически означает, что вы так и не знаете, что же делать с исключением в данном фрагменте кода. В этом разделе мы рассмотрим некоторые тонкости и осложнения, порождаемые контролируемыми исключениями, и варианты работы с последними.
Несмотря на кажущуюся простоту, проблема не только очень сложна, но и к тому же неоднозначна. Существуют твердые приверженцы обеих точек зрения, которые считают, что верный ответ (их) очевиден и просто бросается в глаза. Вероятно, одна из точек зрения основана на несомненных преимуществах перехода от слабо типизированного языка (например, C до выхода стандарта ANSI) к языку с строгой статической проверкой типов (то есть с проверкой во время компиляции), подобному C++ или Java. Преимущества такого перехода настолько очевидны, что строгая статическая проверка типов кажется панацеей от всех бед. Я надеюсь поставить под вопрос ту небольшую часть моей эволюции, отличающуюся абсолютной верой в строгую статическую проверку типов: без сомнения, большую часть времени она приносит пользу, но существует неформальная граница, за которой такая проверка становится препятствием на вашем пути (одна из моих любимых цитат такова: «Все модели неверны, но некоторые полезны»).
Предыстория
Обработка исключений зародилась в таких системах, как PL/1 и Mesa, а затем мигрировала в CLU, Smalltalk, Modula-3, Ada, Eiffel, С++, Python, Java и в появившиеся после Java языки Ruby и С#. Конструкции Java сходны с конструкциями C++, кроме тех аспектов, в которых решения C++ приводили к проблемам.
Обработка исключений была добавлена в C++ на довольно позднем этапе стандартизации. Модель исключений в C++ в основном была заимствована из CLU. Впрочем, в то время существовали и другие языки с поддержкой обработки исключений: Ada, Smalltalk (в обоих были исключения, но отсутствовали их спецификации) и Modula-З (в котором существовали и исключения, и их спецификации).
Следуя подходу CLU при разработке исключений C++, Страуструп считал, что основной целью является сокращение объема кода восстановления после ошибки. Вероятно, он видел немало программистов, которые не писали код обработки ошибок на C, поскольку объем этого кода был устрашающим, а размещение выглядело нелогично. В результате все происходило в стиле C: ошибки в коде игнорировались, а с проблемами справлялись при помощи отладчиков. Чтобы исключения реально заработали, C-программисты должны были писать «лишний» код, без которого они обычно обходились. Таким образом, объем нового кода не должен быть чрезмерным. Важно помнить об этих целях, говоря об эффективности контролируемых исключений в Java.
C++ добавил к идее CLU дополнительную возможность: спецификации исключений, то есть включение в сигнатуру метода информации об исключениях, возникающих при вызове. В действительности спецификация исключения несет двойной смысл. Она означает: «Я возбуждаю это исключение в коде, а вы его обрабатываете». Но она также может означать: «Я игнорирую исключение, которое может возникнуть в моем коде; обеспечьте его обработку». При освещении механизмов исключений мы концентрировались на «обеспечении обработки», но здесь мне хотелось бы поближе рассмотреть тот факт, что зачастую исключения игнорируются, и именно этот факт может быть отражен в спецификации исключения.
В C++ спецификация исключения не входит в информацию о типе функции. Единственная проверка, осуществляемая во время компиляции, относится к согласованному использованию исключений: к примеру, если функция или метод возбуждает исключения, то перегруженная или переопределенная версия должна возбуждать те же самые исключения. Однако, в отличие от Java, компилятор не проверяет, действительно ли функция или метод возбуждают данное исключение, или полноту спецификации (то есть описывает ли она все исключения, возможные для этого метода). Если возбуждается исключение, не входящее в спецификацию, программа на C++ вызывает функцию unexpected() из стандартной библиотеки.
Интересно отметить, что из-за использования шаблонов (templates) спецификации исключений отсутствуют в стандартной библиотеке C++. В Java существуют ограничения на использование параметризованных типов со спецификациями исключений.
Перспективы
Во-первых, язык Java, по сути, стал первопроходцем в использовании контролируемых исключений (несомненно из-за спецификаций исключений C++ и того факта, что программисты на C++ не уделяли им слишком много внимания). Это был эксперимент, повторить который с тех пор пока не решился еще ни один язык.
Во-вторых, контролируемые исключения однозначно хороши при рассмотрении вводных примеров и в небольших программах. Оказывается, что трудноуловимые проблемы начинают проявляться при разрастании программы. Конечно, программы не разрастаются тут же и сразу, но они имеют тенденцию расти незаметно. И когда языки, не предназначенные для больших проектов, используются для небольших, но растущих проектов, мы в некоторый момент с удивлением обнаруживаем, что ситуация изменилась с управляемой на затруднительную в управлении. Именно это, как я полагаю, может произойти, когда проверок типов слишком много, и особенно в отношении контролируемых исключений.
Одним из важных достижений Java стала унификация модели передачи информации об ошибках, так как обо всех ошибках сообщается посредством исключений. В C++ этого не было, из-за обратной совместимости с C и возможности задействовать старую модель простого игнорирования ошибок. Когда Java изменил модель C++ так, что сообщать об ошибках стало возможно только посредством исключений, необходимость в дополнительных мерах принуждения в виде контролируемых исключений сократилась.
В прошлом я твердо считал, что для разработки надежных программ необходимы и контролируемые исключения, и строгая статическая проверка типов. Однако опыт, полученный лично и со стороны1, с языками, более динамичными, чем статичными, привел меня к мысли, что на самом деле главные преимущества обусловлены следующими аспектами:
- Унификация модели сообщения об ошибках посредством исключений (независимо от того, заставляет ли компилятор программиста их обрабатывать).
- Проверка типов, не привязанная к тому, когда она проводится — на стадии компиляции или во время работы программы.
Вдобавок снижение ограничений времени компиляции весьма положительно отражается на продуктивности программиста. С другой стороны, для компенсации чрезмерной жесткости статической проверки типов необходимы рефлексия и параметризация, как вы убедитесь в некоторых примерах книги.
Некоторые уверяли меня, что все сказанное является кощунством, безнадежно испортит мою репутацию, приведет к гибели цивилизации и провалу большой доли программных проектов. Вера в то, что выявление ошибок на стадии компиляции спасет ваш проект, весьма сильна, но гораздо важнее сознавать ограничения того, на что способен компьютер. Стоит помнить:
«Хороший язык программирования помогает программистам писать хорошие программы. Ни один из языков программирования не может запретить своим пользователям писать плохие программы».
В любом случае исчезновение когда-либо из Java контролируемых исключений весьма маловероятно. Это слишком радикальное изменение языка, и защитники их в Sun весьма сильны. История Sun неотделима от политики абсолютной обратной совместимости — фактически любое программное обеспечение Sun работает на любом оборудовании Sun, как бы старо оно ни было. Но, если вы чувствуете, что контролируемые исключения становятся для вас препятствием (особенно если вас заставляют обрабатывать исключение, а вы не знаете, как с ним поступить), существует несколько вариантов.
Передача исключений на консоль
В несложных программах, как во многих примерах данной книги, простейшим решением является передача исключения за пределы метода main(), на консоль. К примеру, при открытии файла для чтения (подробности вы вскоре узнаете) необходимо открыть и закрыть поток FilelnputStream, который возбуждает исключения. В небольшой программе можно поступить следующим образом (подобный подход характерен для многих примеров книги):
//: exceptions/MainException.java
import java.io.*;
public class MainException {
// Передаем все исключения на консоль:
public static void main(String[] args) throws Exception {
// Открываем файл :
FileInputStream file =
new FileInputStream("MainException.java");
// Используем файл ...
// Закрываем файл:
file.close();
}
}
Заметьте, что main() — такой же метод, как и все прочие; он тоже может иметь спецификацию исключений, и здесь типом исключения является Exception, базовый класс всех контролируемых исключений. Передавая его на консоль, вы освобождаетесь от необходимости написания предложений try-catch в теле метода main().
Преобразование контролируемых исключений в неконтролируемые
Рассмотренный выше подход хорош при написании метода main(), но в более общих ситуациях не слишком полезен. Подлинная проблема возникает при написании тела самого обычного метода, когда при вызове другого метода вы четко сознаете: «Понятия не имею, что делать с исключением дальше, но „съедать" мне его не хочется, так же как и печатать банальное сообщение». Проблема решается при помощи цепочек исключений. Управляемое исключение просто «заворачивается» в класс RuntimeException примерно так:
try {
// ... делаем что-нибудь полезное
} саtch (НеЗнаюЧтоДелатьСЭтимКонтролируемымИсключением е) { throw new RuntimeException(e);
}
Решение идеально подходит для тех случаев, когда вы хотите «подавить» контролируемое исключение: вы не «съедаете» его, вам не приходится описывать его в своей спецификации исключений, и благодаря цепочке исключений вы не теряете информацию об исходном исключении.
Описанная методика позволяет игнорировать исключение и пустить его «всплывать» вверх по стеку вызова без необходимости писать блоки try-catch и (или) спецификации исключения. Впрочем, при этом вы все равно можете перехватить и обработать конкретное исключение, используя метод getCause(), как показано ниже:
//: exceptions/TurnOffChecking.java
// "Подавление" контролируемых исключений
import java.io.*;
import static net.mindview.util.Print.*;
class WrapCheckedException {
void throwRuntimeException(int type) {
try {
switch(type) {
case 0: throw new FileNotFoundException();
case 1: throw new IOException();
case 2: throw new RuntimeException("Where am I?");
default: return;
}
} catch(Exception e) { // Превращаем в неконтролируемое:
throw new RuntimeException(e);
}
}
}
class SomeOtherException extends Exception {}
public class TurnOffChecking {
public static void main(String[] args) {
WrapCheckedException wce = new WrapCheckedException();
// Можно вызвать throwRuntimeException() без блока try
// и позволить исключению RuntimeException покинуть метод:
wce.throwRuntimeException(3);
// Или перехватить исключение:
for(int i = 0; i < 4; i++)
try {
if(i < 3)
wce.throwRuntimeException(i);
else
throw new SomeOtherException();
} catch(SomeOtherException e) {
print("SomeOtherException: " + e);
} catch(RuntimeException re) {
try {
throw re.getCause();
} catch(FileNotFoundException e) {
print("FileNotFoundException: " + e);
} catch(IOException e) {
print("IOException: " + e);
} catch(Throwable e) {
print("Throwable: " + e);
}
}
}
}
<spoiler text="Output:">
FileNotFoundException: java.io.FileNotFoundException
IOException: java.io.IOException
Throwable: java.lang.RuntimeException: Where am I?
SomeOtherException: SomeOtherException
</spoiler>
Метод WrapCheckedException.throwRuntimeException() содержит код, генерирующий различные типы исключений. Они перехватываются и «заворачиваются» в объекты RuntimeException, становясь таким образом «причиной» этих исключений.
При взгляде на класс TurnOffChecking нетрудно заметить, что вызвать метод throwRuntimeException() можно и без блока try, поскольку он не возбуждает никаких контролируемых исключений. Но когда вы будете готовы перехватить исключение, у вас будет возможность перехватить любое из них — достаточно поместить свой код в блок try. Начинаете вы с перехвата исключений, которые, как вы знаете, могут явно возникнуть в коде блока try, — в нашем случае первым делом перехватывается SomeOtherException. В конце вы перехватываете RuntimeException и заново возбуждаете исключение, являющееся его причиной (получая последнее методом getCause(), «завернутое» исключение). Так извлекаются изначальные исключения, обрабатываемые в своих предложениях catch.
Методика «заворачивания» управляемых исключений в объекты RuntimeException встречается в некоторых примерах книги. Другое возможное решение — создание собственного класса, производного от RuntimeException. Перехватывать такое исключение не обязательно, но, если вы захотите, такая возможность существует.
Основные правила обработки исключений
Используйте исключения для того, чтобы:
- обработать ошибку на текущем уровне (избегайте перехватывать исключения, если вы не знаете, как с ними поступить);
- исправить проблему и снова вызвать метод, возбудивший исключение;
- предпринять все необходимые действия и продолжить выполнение без повторного вызова метода;
- попытаться найти альтернативный результат вместо того, который должен был бы произвести вызванный метод;
- сделать все возможное в текущем контексте и заново возбудить это же исключение, перенаправив его на более высокий уровень;
- сделать все, что можно в текущем контексте, и возбудить новое исключение, перенаправив его на более высокий уровень;
- завершить работу программы;
- упростить программу (если используемая вами схема обработки исключений делает все только сложнее, значит, она никуда не годится);
- добавить вашей библиотеке и программе безопасности (сначала это поможет в отладке программы, а в дальнейшем окупится ее надежностью).
Резюме
Исключения являются неотъемлемой частью программирования на Java; существует некий барьер, который невозможно преодолеть без умения работать с ними. По этой причине исключения были представлены именно в этой части книги — многими библиотеками (скажем, библиотекой ввода/вывода) просто невозможно нормально пользоваться без обработки исключений.
Одно из преимуществ обработки исключений состоит в том, что она позволяет сосредоточиться на решаемой проблеме, а затем обработать все ошибки в описанном коде в другом месте. Хотя исключения обычно описываются как средство передачи информации и восстановления после ошибок на стадии выполнения, я сильно сомневаюсь, что «восстановление» часто реализуется на практике. По моей оценке, это происходит не более чем в 10% случаев, и даже тогда в основном сводится к раскрутке стека к заведомо стабильному состоянию вместо реального выполнения действий по восстановлению. На мой взгляд, ценность исключений в основном обусловлена именно передачей информации. Java фактически настаивает, что программа должна сообщать обо всех ошибках в виде исключений, и именно это обстоятельство обеспечивает Java большое преимущество перед языками вроде C++, где программа может сообщать об ошибках разными способами (а то и не сообщать вовсе).
------------------------
ТРИО теплый пол отзыв
Заработок на сокращении ссылок
Earnings on reducing links
Код PHP на HTML сайты
Категория: Книги по Java
Комментарии |