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

Джош Блох

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

Глава 8 Общие вопросы программирования

с классами, интерфейсами и методами, поскольку хорошо спроекти­ рованный A PI если и предоставляет какое-либо поле, то немного. Поля типа boolean обычно именуются так же, как логические методы доступа, но префикс «is» у них опускается, например initialized, composite. Поля других типов, как правило, именуются с помощью существительного или именной конструкции, например height, dig­ its, bodyStyle. Грамматические соглашения для локальных перемен­ ных аналогичны соглашениям для полей, только их соблюдение еще менее обязательно.

Подведем итоги. Изучите стандартные соглашения по имено­ ванию и доведите их использование до автоматизма. Типографские соглашения просты и практически однозначны; грамматические соглашения более сложные и свободные. Как сказано в «The Java Language Specification» [JLS, 6.8], не нужно рабски следовать этим соглашениям, если длительная практика их применения диктует иное решение. Пользуйтесь здравым смыслом.

330

Исключения

Если исключения (exception) используются наилучшим образом,

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

Используйте исключения лишь в исключительных ситуациях

Однажды, если вам не повезет, вы сделаете ошибку в програм­ ме, например такую:

//Неправильное использование исключений. Никогда так

//не делайте!

try {

int i = 0; while(true)

range[i++].climb();

} catch(ArrayIndexOutOfBoundsException e) {

}

331

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

Что делает этот код? Изучение кода не вносит полной ясности, и это достаточная причина, чтобы им не пользоваться. Здесь при­ ведена плохо продуманная идиома для циклического перебора эле­ ментов в массиве. Когда производится попытка обращения к перво­ му элементу за пределами массива, бесконечный цикл завершается инициированием исключительной ситуации ArraylndexOutOfBoundException, ее перехватом и последующим игнорированием. Предпола­ гается, что это эквивалентно стандартной идиоме цикла по массиву, которую узнает любой программист Java:

for (Mountain m : range)

m.climbO;

Но почему же кто-то выбрал идиому, использующую исключе­ ния, вместо другой, испытанной и правильной? Это вводящая в за­ блуждение попытка улучшить производительность, которая исходит из ложного умозаключения, что, поскольку виртуальная машина про­ веряет границы при всех обращениях к массиву, обычная проверка на завершение цикла (i < a. length) избыточна и ее следует устра­ нить. В этом рассуждении неверны три момента:

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

Размещение кода внутри блока try-catch препятствует вы ­ полнению определенных процедур оптимизации, кото­ рые в противном случае могли бы быть исполнены в со­ временных реализациях JV M .•

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

впроцессе оптимизации некоторые современные реали­ зации JV M отбрасывают их.

332

С тать я 57

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

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

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

Этот принцип относится также к проектированию API. Хорошо спроектированный A PI не должен заставлять своих клиентов исполь­ зовать исключения для обычного управления потоком вычислений. Если в классе есть метод, зависящий от состояния (state -dependent) , который может быть вызван лишь при выполнении определенных не­ предсказуемых условий, то в этом же классе, как правило, должен

333

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

присутствовать отдельный метод, проверяющий состояние ( state- testing), который показывает, можно ли вызывать первый метод. Н а­ пример, класс Iterator имеет зависящий от состояния метод next, ко­ торый возвращает элемент для следующего прохода цикла, а также соответствующий метод проверки состояния hasNext. Это позволяет применять для просмотра коллекции в цикле следующую стандарт­ ную идиому:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { Foo foo = i.next();

}

Если бы в классе Iterator не было метода hasNext, клиент был бы вынужден использовать следующую конструкцию:

//Не пользуйтесь этой отвратительной идиомой

//для просмотра коллекции в цикле!

try {

Iterator<Foo> i = collection.iterator(); while(true) {

Foo foo = i.next();

}

} catch (NoSuchElementException e) {

}

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

В качестве альтернативы отдельному методу проверки состояния можно использовать особый зависящий от состояния метод: он будет возвращать особое значение, например null, при вызове для объекта, имеющего неподходящее состояние. Для класса Iterator этот прием

334

С татья 58

не годится, поскольку null является допустимым значением для ме­

тода next.

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

Подведем итоги. Исключения созданы для использования в ис­ ключительных условиях. Не используйте их для обычного контроля и не пишите такие A PI, которые будут заставлять других это делать.

'00*4_ Применяйте обрабатываемые исключения для восстановления, для программных ошибок используйте исключения времени выполнения

В языке программирования Java предусмотрены три типа объек­ тов Throwable: обрабатываемые исключения (checked exception), ис­ ключения времени выполнения (run-time exception) и ошибки (error). Программисты обычно путают, при каких условиях следует исполь­ зовать каждый из этих типов. Решение не всегда очевидно, но есть несколько общих правил, в значительной мере упрощающих выбор.

Основное правило при выборе между обрабатываемым и необра­ батываемым исключениями гласит: используйте обрабатываемые ис-

335

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

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

Предоставляя пользователю A PI обрабатываемое исключение, разработчик A PI передает ему право осуществлять обработку соот­ ветствующего условия. Пользователь может пренебречь этим пра­ вом, перехватив исключение и проигнорировав его. Однако, как пра­ вило, это оказывается плохим решением (статья 65).

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

Используйте исключения времени выполнения для индикации программных ошибок. Подавляющее большинство исключений вре­ мени выполнения сообщает о нарушении предусловий (precondition violation). Нарушение предусловия означает лишь то, что клиент A PI не смог выполнить соглашения, заявленные в спецификации к этому API. Например, в соглашениях для доступа к массиву оговаривается, что индекс массива должен попадать в интервал от нуля до «длина массива минус один». Исключение ArraylndexOutOf Bounds указывает, что это предусловие было нарушено.

Хотя в спецификации языка Java это не оговорено, существует строго соблюдаемое соглашение о том, что ошибки зарезервированы

336

С татья 58

в JV M для того, чтобы фиксировать дефицит ресурсов, нарушение инвариантов и другие условия, делающие невозможным дальнейшее выполнение программы [Chan98, HorstmanOO]. Поскольку эти со­ глашения признаны практически повсеместно, лучше для Error во­ обще не создавать новых подклассов. Все реализуемые вами необра­ батываемые исключения должны прямо или косвенно наследовать

класс RuntimeException.

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

не наследует классов Exception, RuntimeException и Error. В специ­

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

Подведем итоги. Для ситуаций, когда можно обработать ошиб­ ку и продолжить исполнение, используйте обрабатываемые исклю­ чения, для программных ошибок применяйте исключения времени выполнения. Разумеется, ситуация не всегда однозначна, как белое и черное. Рассмотрим случай с исчерпанием ресурсов, которое может быть вызвано программной ошибкой, например размещением в па­ мяти неоправданно большого массива, или настоящим дефицитом ресурсов. Если исчерпание ресурсов вызвано временным дефицитом или временным увеличением спроса, эти условия вполне могут быть изменены. Именно разработчик API принимает решение, возможно ли восстановление работоспособности программы в конкретном слу­ чае исчерпания ресурсов. Если вы считаете, что работоспособность можно восстановить, используйте обрабатываемое исключение. В противном случае применяйте исключение времени выполнения. Если неясно, возможно ли восстановление, то по причинам, описан­ ным в статье 59, лучше остановиться на необрабатываемом исклю­ чении.

337

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

Разработчики API часто забывают, что исключения — это впол­ не законченные объекты, для которых можно определять любые ме­ тоды. Основное назначение таких методов — создание кода, который увязывал бы исключение с дополнительной информацией об условии, вызвавшем появление данной исключительной ситуации. Если таких методов нет, программистам придется разбираться со строковым представлением этого исключения, выуживая из него дополнитель­ ную информацию. Эта крайне плохая практика. Классы редко ука­ зывают какие-либо детали в своем строковом представлении, само строковое представление может меняться от реализации к реализа­ ции, от версии к версии. Следовательно, программный код, который анализирует строковое представление исключения, скорее всего ока­ жется непереносимым и ненадежным.

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

Избегайте ненужных обрабатываемых исключений

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

338

С тать я 59

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

Такое решение оправданно, если даже при надлежащем приме­ нении интерфейса API невозможно предотвратить возникновение условий для исключительной ситуации, однако программист, поль­ зующийся данным API, столкнувшись с этим исключением, мог бы предпринять какие-либо полезные действия. Если не выполняют­ ся оба этих условия, лучше пользоваться необрабатываемым исклю­ чением. Роль лакмусовой бумажки в данном случае играет вопрос: как программист будет обрабатывать исключение? Является ли это решение лучшим:

} catch(TheCheckedException е) { throw new Error("Assertion error");

// Условие не выполнено. Этого не должно быть никогда!

}

Ачто скажете об этом:

}catch(TheCheckedException е) {

е.printStackTrace(); //Ладно, закончили работу.

System.exit(1);

}

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

ся CloneNot Sup ported Except ion. Оно инициируется методом Object,

clone, который должен использоваться лишь для объектов, реали­ зующих интерфейс Cloneable (статья И). Блок catch практически всегда соответствует невыполнению утверждения. Так что обрабаты­ ваемое исключение не дает программисту преимуществ, но требует от последнего дополнительных усилий и усложняет программу.

339

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]