52

52


}
return s;
}
public void disposeO { try {
in.closeO;
System.out.printlnC'disposeO успешен"); } catch(IOException e2) {
throw new RuntimeExceptionC'in.closeO исполнен неудачно");
}
}
} ///:-
Конструктор 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 действительно будет завершена. Казалось бы, подобные действия удобно разместить в методе fina- lize(), но, как упоминалось в главе 5, вызов этого метода не гарантирован (и даже если вы знаете, что он будет вызван, то неизвестно, когда). Это один из недостатков Java: все завершающие действия, кроме освобождения памяти, не производятся автоматически, так что вам придется информировать пользо¬вателя о том, что он ответственен за их выполнение.
Самый безопасный способ использования класса, который способен выдать исключение при конструировании и требует завершающих действий, основан на использовании вложенных блоков try:
//: exceptions/Cleanup.java
// Гарантированное освобождение ресурсов.
public class Cleanup {
public static void main(String[] args) { try {
InputFile in = new InputFileC'Cleanup java"); try {
String s; int i = 1;
whileC(s = in getLineO) != null) ; // Построчная обработка .. } catch(Exception e) {
System.out.println("Перехвачено Exception в main"). e.printStackTrace(System.out); } finally {
in.disposeO;
}
} catch(Exception e) {
System out println("Сбой при конструировании InputFile"):
}
}
} /* Output:
disposeO успешен
*///:-
Присмотритесь к логике происходящего: конструирование объекта InputFile фактически заключено в собственный блок try. Если попытка завершается не¬удачей, мы входим во внешнюю секцию catch и метод dispose() не вызывается. Но, если конструирование прошло успешно, мы хотим обеспечить гарантиро¬ванное завершение, поэтому сразу же после конструирования создается новый блок try. Блок finally, выполняющий завершение, связывается с внутренним блоком try; таким образом, блок finally не выполняется при неудачном конст¬руировании и всегда выполняется, если конструирование прошло удачно.
Эта универсальная идиома применяется и в тех ситуациях, когда конструк¬тор не выдает исключений. Основной принцип: сразу же после создания объекта, требующего завершения, начинается конструкция try-finally:
//: exceptions/Cleanupldiom java
// За каждым освобождаемым объектом следует try-finally
class NeedsCleanup { // Конструирование не может завершиться неудачно private static long counter = 1,
private final long id = counter++, Л
продолжение &
pub.lic void disposeO {
System out printin("NeedsCleanup " + id + " завершен");
class ConstructionException extends Exception {}
class NeedsCleanup2 extends NeedsCleanup { // Возможны сбои при конструировании, public NeedsCleanup2() throws ConstructionException {}
public class Cleanupldiom {
public static void main(String[] args) { // Секция 1-
NeedsCleanup ncl = new NeedsCleanupO; try {
// .. } finally {
ncl.disposeO.
// Секция 2;
// Если сбои при конструировании исключены, // объекты можно группировать. NeedsCleanup nc2 = new NeedsCleanupO; NeedsCleanup псЗ = new NeedsCleanupO; try {
// .. } finally {
nc3 disposeO; // Обратный порядок конструирования nc2.disposeO;
// Секция 3-
// Если при конструировании возможны сбои, каждый объект // защищается отдельно; try {
NeedsCleanup2 nc4 = new NeedsCleanup20; try {
NeedsCleanup2 nc5 = new NeedsCleanup2(); try {
// ...
} finally {
nc5.disposeO;
}
} catch(ConstructionException e) { // Конструктор nc5
System.out.println(e), } finally {
nc4 disposeO;
}
} catch(ConstructionException e) { // Конструктор nc4 System.out.println(e);
}
}
} /* Output; NeedsCleanup 1 завершен NeedsCleanup 3 завершен
Идентификация исключений 343
NeedsCleanup 2 завершен NeedsCleanup 5 завершен NeedsCleanup 4 завершен */// ~
Секция 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) { // Перехват точного типа try {
throw new SneezeO; } catch(Sneeze s) {
System out println("Перехвачено Sneeze"). } catch(Annoyance a) {
System 0ut.println("nepexBa4eH0 Annoyance"),
}
// Перехват базового типа try {
throw new SneezeO. } catch(Annoyance a) {
System out рпп^пС'Перехвачено Annoyance").
}
}
Перехвачено Sneeze Перехвачено Annoyance *///•-
Исключение Sneeze будет перехвачено в первом блоке catch, который ему со¬ответствует — конечно, это будет первый блок. Но, если удалить первый блок catch, оставив только проверку Annoyance, программа все равно работает, пото¬му что она перехватывает базовый класс Sneeze. Другими словами, блок catch (Annoyance а) поймает Annoyance или любой другой класс, унаследованный от не¬го. Если вы добавите новые производные исключения в свой метод, программа пользователя этого метода не потребует изменений, так как клиент перехваты¬вает исключения базового класса.
Если вы попытаетесь «замаскировать» исключения производного класса, по¬местив сначала блок catch базового класса:
try {
throw new SneezeO;
} catch(Annoyance a) { // ..
} catch(Sneeze s) { II...
}
компилятор выдаст сообщение об ошибке, так как он видит, что блок catch для исключения Sneeze никогда не выполнится.
Альтернативные решения
Система обработки исключений представляет собой «черный ход», позволяю¬щий программе нарушить нормальную последовательность выполнения ко¬манд. «Черный ход» открывается при возникновении «исключительных ситуа¬ций», когда обычная работа далее невозможна или нежелательна. Исключения представляют собой условия, с которыми текущий метод справиться не в со¬стоянии. Причина, по которой возникают системы обработки исключений, кро¬ется в том, что программисты не желали иметь дела с громоздкой проверкой всех возможных условий возникновения ошибок каждой функции. В результа¬те ошибки ими просто игнорировались. Стоит отметить, что вопрос удобства программиста при обработке ошибок стоял на первом месте для разработчиков Java.
Основное правило при использовании исключений гласит: «Не обрабаты¬вайте исключение, если вы не знаете, что с ним делать». По сути, отделение кода, ответственного за обработку ошибок, от места, где ошибка возникает, яв¬ляется одной из главных целей обработки исключений. Это позволяет вам скон¬центрироваться на том, что вы хотите сделать в одном фрагменте кода, и на том, как вы собираетесь поступить с ошибками в совершенно другом месте програм¬мы. В результате основной код не перемежается с логикой обработки ошибок, что упрощает его сопровождение и понимание. Исключения также сокращают объем кода, так как один обработчик может обслуживать несколько потенци¬альных источников ошибок.
Контролируемые исключения немного усложняют ситуацию, поскольку они заставляют добавлять обрабатывающие исключения предложения там, где вы не всегда еще готовы справиться с ошибкой. В итоге возникает проблема «про¬глоченных исключений»:
try {
// ^ делает что-то полезное
} са^И(6бязывающееИсключение е) {} // Проглотили!
Программисты (и я в том числе, в первом издании книги), не долго думая, делали самое бросающееся в глаза и «проглатывали» исключение — зачастую непреднамеренно, но, как только дело было сделано, компилятор был удовле¬творен, поэтому пока вы не вспоминали о необходимости пересмотреть и ис¬править код, не вспоминали и об исключении. Исключение происходит, но безвозвратно теряется. Из-за того что компилятор заставляет вас писать код для обработки исключений прямо на месте, это кажется самым простым реше¬нием, хотя на самом деле ничего хуже и придумать нельзя.
Ужаснувшись тем, что я так поступил, во втором издании книги я «испра¬вил» проблему, распечатыв в обработчике трассировку стека исключения (и сейчас это можно видеть — в подходящих местах — в некоторых примерах данной главы). Хотя это и полезно при отслеживании поведения исключений, трассировка фактически означает, что вы так и не знаете, что же делать с ис¬ключением в данном фрагменте кода. В этом разделе мы рассмотрим некоторые тонкости и осложнения, порождаемые контролируемыми исключениями, и ва¬рианты работы с последними.
Несмотря на кажущуюся простоту, проблема не только очень сложна, но и к тому же неоднозначна. Существуют твердые приверженцы обеих точек зрения, которые считают, что верный ответ (их) очевиден и просто бросается в глаза. Вероятно, одна из точек зрения основана на несомненных преимущест¬вах перехода от слабо типизированного языка (например, С до выхода стандар¬та ANSI) к языку с строгой статической проверкой типов (то есть с проверкой во время компиляции), подобному С++ или Java. Преимущества такого перехо¬да настолько очевидны, что строгая статическая проверка типов кажется пана¬цеей от всех бед. Я надеюсь поставить под вопрос ту небольшую часть моей эво¬люции, отличающуюся абсолютной верой в строгую статическую проверку типов: без сомнения, большую часть времени она приносит пользу, но сущест¬вует неформальная граница, за которой такая проверка становится препятстви¬ем на вашем пути (одна из моих любимых цитат такова: «Все модели неверны, но некоторые полезны»).