Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27 Мб
Скачать
☆

Глава 9 • Исключения

Дополнительные действия со стороны программиста, связанные с обработкой обрабатываемого исключения, значительно увеличива­ ются, если это единственное исключение, инициируемое данным мето­ дом. Если есть другие исключения, метод будет стоять в блоке tгу, так что для этого исключения понадобится всего лишь еще один блок catch. Если же метод инициирует только одно обрабатываемое исключение, оно будет требовать, чтобы вызов соответствующего метода был поме­ щен в блок t гу. В таких условиях имеет смысл подумать: не существует ли какого-либо способа избежать обрабатываемого исключения.

Один из приемов, позволяющих превратить обрабатываемое исключение в необрабатываемое, состоит в разбиении метода, ини­ циирующего исключение, на два метода, первый из которых будет возвращать булево значение, указывающее, будет ли инициировать­ ся исключение. Таким образом, в результате преобразования API по­ следовательность вызова

// Вызов с обрабатываемым исключением try {

obj.action(args);

}catch(TheCheckedException e) {

//Обработать исключительную ситуацию

}

принимает следующий вид:

//Вызов с использованием метода проверки состояния

//и необрабатываемого исключения

if (obj.actionPermitted(args)) { obj.action(args);

}else{

// Обработать исключительную ситуацию

}

Такое преобразование можно использовать не всегда. Если же оно допустимо, это может сделать работу с A PI более удобной.

340

С тать я 60

Хотя второй вариант последовательности вызова выглядит не луч­ ше первого, полученный A PI имеет большую гибкость. В ситуации, когда программист знает, что вызов будет успешным, или согласен на завершение потока в случае неудачного вызова, преобразованный API позволяет использовать следующую упрощенную последова­ тельность вызова:

obj.action(args);

Если вы предполагаете, что применение упрощенной после­ довательности вызова будет нормой, то описанное преобразование A PI приемлемо. API, полученный в результате этого преобразо­ вания, в сущности, тот же самый, что и API с методом «проверки состояния» (статья 57). Следовательно, к нему относятся те же са­ мые предупреждения: если к объекту одновременно и без внешней синхронизации могут иметь доступ сразу несколько потоков или этот объект может менять свое состояние по команде извне, ука­ занное преобразование использовать не рекомендуется. Это связа­ но с тем, что в промежутке между вызовом actionPermitted и вызо­ вом action состояние объекта может успеть поменяться. Если метод actionPermitted при необходимости и мог бы дублировать работу ме­ тода action, то от преобразования, вероятно, стоит отказаться по со­ ображениям производительности.

Предпочитайте стандартные исключения

Одной из сильных сторон экспертов, отличающих их от ме­ нее опытных программистов, является то, что эксперты борются за высокую степень повторного использования программного кода и обычно этого добиваются. Общее правило, гласящее, что повторно используемый код —это хорошо, относится и к технологии исклю­ чений. В библиотеках для платформы Java реализован основной на­ бор необрабатываемых исключений, перекрывающий большую часть

341

Глава 9 • Исключения

потребностей в исключениях для API. В этой статье обсуждаются наиболее часто применяемые исключения.

Повторное использование уже имеющихся исключений имеет не­ сколько преимуществ. Главное то, что они упрощают освоение и при­ менение вашего API, поскольку соответствуют установленным согла­ шениям, с которыми программисты уже знакомы. С этим же связано второе преимущество, которое заключается в том, что программы, ис­ пользующие ваш API, легче читать, поскольку там нет незнакомых, сбивающих с толку исключений. Наконец, чем меньше классов исклю­ чений, тем меньше требуется места в памяти и времени на их загрузку.

Чаще всего используется исключение IllegalArgumentException. Обычно оно инициируется, когда вызываемому методу передается аргумент с неправильным значением. Например, IllegalArgumentEx­ ception может инициироваться в случае, если для параметра, указы­ вающего количество повторов для некоей процедуры, передано отри­ цательное значение.

Другое часто используемое исключение — IllegalStateExcep- tion. Оно обычно инициируется, если в соответствии с состоянием объекта вызов метода является неправомерным. Например, это ис­ ключение может инициироваться, когда делается попытка использо­ вать некий объект до его инициализации надлежащим образом.

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

NullPointerException, а не IllegalArgumentException. Точно так же,

если параметру, который соответствует индексу некоей последова­ тельности, при вызове было передано значение, выходящее за гра­ ницы допустимого диапазона, инициироваться должно исключение

IndexOutOfBoundsException, а не IllegalArgumentException.

342

С тать я 60

Еще одно универсальное исключение, о котором необходимо

знать: ConcurrentModificationException. Оно должно инициировать­

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

Последнее универсальное исключение, заслуживающее упо­

минания, — UnsupportedOperationException. Оно инициируется,

если объект не имеет поддержки производимой операции. По срав­ нению с другими исключениями, обсуждавшимися в этой статье,

UnsupportedOperationException применяется довольно редко, по­

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

В таблице 8.1 собраны самые распространенные из повторно ис­

пользуемых исключений.

Таблица 8.1

Часто используемые исключения

Исключение

Повод для использования

IllegalA rgum entException

Н еправильное значение параметра

IllegalStateException

Состояние объекта неприемлемо для вы зова

 

метода

N ullPointerException

Значение параметра равно null, а это запрещ ено

IndexO utO fB oundsException

Значение параметра, задаю щ его индекс,

 

выходит за пределы диапазона

ConcurrentM odificationException

О бнаруж ена параллельная модификация

 

объекта из разны х потоков, а это запрещ ено

U nsupportedO perationException

О бъ ект не имеет поддержки указанного метода

343

Глава 9 • Исключения

^4\44NSS^S44\\44VSS444\4VS\^\N^^^

Помимо перечисленных исключений, при определенных обсто­ ятельствах могут применяться и другие исключения. Например, при реализации таких арифметических объектов, как комплексные числа и матрицы, уместно пользоваться исключениями ArithmeticException и NumberFormatException. Если исключение отвечает вашим по­ требностям, пользуйтесь им, но только чтобы условия, при которых вы будете его инициировать, не вступали в противоречие с докумен­ тацией к этому исключению. Выбирая исключение, следует исходить из его семантики, а не только из названия. Кроме того, если вы хоти­ те дополнить имеющееся исключение информацией об отказе (статья 63), не стесняйтесь создавать для него подклассы.

И наконец, учитывайте, что выбор исключения — не всегда точ­ ная наука, поскольку «поводы для использования», приведенные в та­ блице 8.1, не являются взаимоисключающими. Рассмотрим, например, объект, соответствующий колоде карт. Предположим, что для него есть метод, осуществляющий выдачу карт из колоды, причем в каче­ стве аргумента ему передается количество требуемых карт. Допустим, что при вызове с этим параметром было передано значение, превы­ шающее количество карт, оставшихся в колоде. Эту ситуацию можно толковать как IllegalArgumentException (значение параметра «размер сдачи» слишком велико) либо как IllegalStateException (объект «ко­ лода» содержит слишком мало карт для обработки запроса). В данном случае, по-видимому, следует использовать IllegalArgumentException, но непреложных правил здесь не существует.

Инициируйте исключения, соответствующие абстракции

Если метод инициирует исключение, не имеющее видимой связи

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

344

С татья 61

версии реализация верхнего уровня поменяется, то также могут по­ меняться и инициируемые им исключения, в результате чего могут перестать работать имеющиеся клиентские программы.

Во избежание этой проблемы верхние уровни приложения должны перехватывать исключения нижних уровней и, в свою очередь, иниции­ ровать исключения, которые можно объяснить в терминах абстракции верхнего уровня. Описываемая идиома, которую мы называем трансля­ цией исключений (exception translation), выглядит следующим образом:

/ / Трансляция исключения

try {

//Использование абстракции нижнего уровня

//для выполнения наших указаний

}catch(LowerLevelException е) {

throw new HigherLevelException(...);

}

Приведем конкретный пример трансляции исключения, взятый из класса Abst ractSequentialList, который представляет собой скелетную реализацию (статья 18) интерфейса List. В этом примере трансляция ис­ ключения продиктована спецификацией метода get в интерфейсе List:

I -к -к

 

 

 

 

* Возвращает

элемент, находящийся в указанной

позиции

* в заданном

списке.

 

 

 

* @throws IndexOutOfBoundsException,

если индекс

находится

* за пределами диапазона (index < 0

|| index

>=

size()).

*/

public Object get(int index) { Listlterator i = listlterator(index); try {

return i.nextO;

} catch (NoSuchElementException e) {

throw new IndexOutOfBoundsException(“Index: " + index);

}

}

345

Глава 9 • Исключения

В тех случаях, когда исключение нижнего уровня может быть по­ лезно при анализе ситуации, вызвавшей исключение, лучше исполь­ зовать особый вид трансляции исключений, называемый сцеплени­ ем исключений (exception chaining). При этом исключение нижнего уровня передается с исключением верхнего уровня; в последнем соз­ дается открытый метод доступа, позволяющий извлечь исключение нижнего уровня:

/ / Сцепление исключений

try {

//Использование абстракции нижнего уровня

//для выполнения наших указаний

}catch(LowerLevelException е) {

throw new HigherLevelException(e);

}

Конструктор исключений верхнего уровня передает cause кон­ структору суперклассов, учитывающему сцепление, так что он окон­ чательно передается конструктору Throwable, учитывающему сце­ пление:

/ / Исключения с помощью конструктора, учитывающего сцепление

class HigherLevelException extends Exception {

HigherLevelException(Throwable cause) {

super(cause);

}

}

У большинства стандартных исключений имеются учитываю­ щие сцепление конструкторы. Для исключений, у которых нет кон­ структоров, вы можете воспользоваться методом initCause объекта Throwable. Не только сцепление исключений дает вам программный доступ к cause (с помощью getCause), но еще он интегрирует его стек в исключение верхнего уровня.

Хотя трансляция исключений лучше, чем бессмысленная пере­ дача исключений с нижних уровней, злоупотреблять ею не следует.

346

С тать я 62

»>»4\\SfrW S»

ч^чЧЧ^УУУХТ^?»!«вдаядУЕЙ\S S № L<№ Sg& *^^4s'№ S>»&W &*'y»^^

Самый хороший способ обработки исключений нижнего уровня — полностью исключить их возможность. Для этого перед вызовом метода нижнего уровня необходимо убедиться в том, что он будет выполнен успешно. Иногда добиться этого можно путем явной проверки аргументов метода верхнего уровня перед их передачей на нижний уровень.

Если предупредить появление исключений на нижних уровнях невозможно, то лучшее решение состоит в том, чтобы верхний уро­ вень молча обрабатывал эти исключения, изолируя клиента от про­ блем нижнего уровня. В таких условиях чаще всего достаточно протоколировать исключения, используя какой-либо механизм ре­ гистрации, например java.util.logging, появившийся в версии 1.4. Это дает возможность администратору исследовать возникшую про­ блему и в то же время изолирует от нее программный код клиента

иконечного пользователя.

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

Для каждого метода документируйте все инициируемые исключения

Описание инициируемых методом исключений составляет важ­ ную часть документации, которая необходима для правильного при­ менения метода. Поэтому крайне важно, чтобы вы уделили время тщательному описанию всех исключений, инициируемых каждым методом.

347

Глава 9 • Исключения

Обрабатываемые исключения всегда декларируйте по отдельно­ сти с помощью тега @throws (Javadoc), четко описывайте условия, при которых каждое из них инициируется. Не пытайтесь сократить описание, объявляя о том, что метод инициирует некий суперкласс исключений, вместо того чтобы декларировать несколько классов возможных исключений. Например, никогда не объявляйте, что ме­ тод инициирует исключение Exception или, что еще хуже, исключе­ ние Throwable. Помимо того, что такая формулировка не дает про­ граммисту никакой информации о том, какие исключения могут быть инициированы данным методом, она значительно затрудняет работу с методом, поскольку надежно перекрывает любое другое исключе­ ние, которое может быть инициировано в этом же месте.

Хотя язык Java не требует, чтобы программисты декларировали необрабатываемые исключения, которые могут быть инициированы данным методом, имеет смысл документировать их столь же тщатель­ но, как и обрабатываемые исключения. Необрабатываемые исклю­ чения обычно представляют ошибки программирования (статья 40), ознакомление программиста со всеми этими ошибками может помочь ему избежать их. Хорошо составленный перечень необрабатываемых исключений, которые может инициировать метод, фактически опи­ сывает предусловия для его успешного выполнения. Важно, чтобы в документации к каждому методу были описаны его предусловия, а описание необрабатываемых исключений как раз и является наилучшим способом выполнения этого требования.

Особенно важно, чтобы для методов интерфейса были описаны необрабатываемые исключения, которые могут быть ими иницииро­ ваны. Такая документация является частью основных соглашений для интерфейса и обеспечивает единообразное поведение различных его реализаций.

Для описания каждого необрабатываемого исключения, кото­ рое может быть инициировано методом, используйте тег @throws (Javadoc), однако не нужно с помощью ключевого слова th rows вклю­ чать необрабатываемые исключения в декларацию метода. Програм­

348

С тать я 62

мист, пользующийся вашим API, должен знать, какие из исключений обрабатываются, а какие — нет, поскольку в первом и втором случаях на него возлагается различная ответственность. Наличие описания, соответствующего тегу @throws, и отсутствие заголовка к методу, соответствующемудекларации throws, создает мощный визуальный сигнал, помогающий программисту отличить обрабатываемые ис­ ключения от необрабатываемых.

Следует отметить, что документирование всех необрабатывае­ мых исключений, которые могут быть инициированы каждым мето­ дом, — это идеал, который не всегда достижим в реальности. Когда производится пересмотр класса и предоставляемый пользователю метод меняется так, что начинает инициировать новые необраба­ тываемые исключения, это не является нарушением совместимости ни на уровне исходных текстов, ни на уровне байт-кода. Предполо­ жим, некий класс вызывает метод из другого класса, написанного не­ зависимо. Авторы первого класса могут тщательно документировать все необрабатываемые исключения, инициируемые каждым методом. Однако, если второй класс был изменен так, что теперь он иниции­ рует дополнительные необрабатываемые исключения, первый класс (не претерпевший изменений) тоже будет передавать эти новые не­ обрабатываемые исключения, хотя он их и не декларировал.

Если одно и то же исключение по одной и то же причине иници­ ируется несколькими методами, его описание можно поместить в об­ щий комментарий к документации для всего класса, а не описывать его отдельно для каждого метода. Примером такого рода является исключение NullPointerException. Прекрасно было бы в коммента­ рии к классу сказать «все методы этого класса инициируют исключе­ ние NullPointerException, если с каким-либо параметром была пере­ дана нулевая ссылка на объект» или другие слова с тем же смыслом.

Подведем итоги. Необходимо документировать все исключения, которые могут быть выведены каждым написанным вами методом. Это относится как к обрабатываемым, так и к необрабатываемым исклю­ чениям, как к абстрактным, так и к конкретным методам. Для каждого

349