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

Джош Блох

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

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

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

В описание исключения добавляйте информацию о сбое

Если выполнение программы завершается аварийно из-за не­ обработанного исключения, система автоматически распечатывает трассировку стека для этого исключения. Трассировка стека содер­ жит строковое представление данного исключения, результат вы­ зова его метода toSt ring. Обычно это представление состоит из на­ звания класса исключения и описания исключения (detail message).

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

Для фиксации сбоя строковое представление исключения долж­ но содержать значения всех параметров и полей, «способствовавших появлению этого исключения». Например, описание исключения 1п- dexOutOfBounds должно содержать нижнюю границу, верхнюю границу и действительный индекс, который не уложился в эти границы. Такая информация говорит об отказе очень многое. Любое из трех значений или все они вместе могут быть неправильными. Представленный ин­ декс может оказаться на единицу меньше нижней границы или быть равен верхней границе («ошибка границы» — fencepost error) либо мо-

350

С тать я 63

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

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

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

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

j **

* Конструируем IndexOutOfBoundsException

*@param lowerBound - самое меньшее из разрешенных значений

*индекса

351

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

*@рагаш upperBound - самое большее из разрешенных значений

*индекса

*

плюс

один

 

* @param

index

- действительное значение индекса

*/

 

 

 

public IndexOutOfBoundsException(int lowerBound,

int upperBound,

 

 

int index)

{

//Генерируем описание исключения,

//фиксирующее обстоятельства отказа super( “Lower bound: “ + lowerBound +

Upper bound: “ + upperBound +

 

Index: “

+ index);

// Сохраняем

информацию об ошибке для программного доступа

this.lowerBound

=

lowerBound;

this.upperBound

=

upperBound;

this.index =

index;

 

}

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

Как отмечалось в статье 58, возможно, имеет смысл, чтобы ис­ ключение предоставляло методы доступа к информации об обстоя­ тельствах сбоя (в представленном выше примере это lowerBound, upperBound и index). Наличие таких методов доступа для обраба­ тываемых исключений еще важнее, чем для необрабатываемых, по­ скольку информация об обстоятельствах сбоя может быть полезна для восстановления работоспособности программы. Программный доступ к деталям необрабатываемого исключения редко интересует программистов (хотя это и не исключено). Однако, согласно общему

352

С тать я 64

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

Добивайтесь атомарности методов по отношению к сбоям

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

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

Для методов, работающих с изменяемыми объектами, атомар­ ность по отношению к сбою чаще всего достигается путем проверки правильности параметров перед выполнением операции (статья 38). Благодаря этому любое исключение будет инициироваться до того, как начнется модификация объекта. В качестве примера рассмотрим метод Stack, pop из статьи 6:

public Object рор() { if (size == 0)

353

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

throw new EmptyStackException(); Object result = elements[-size];

elements[size] = null; // Убираем устаревшую ссылку return result;

}

Е сли убрать начальную проверку размера, метод все равно будет инициировать исключение при попытке получить элемент из пустого стека. Однако при этом он будет оставлять поле size в неопределен­ ном (отрицательном) состоянии. А это приведет к тому, что сбоем будет завершаться вызов любого метода в этом объекте. Кроме того, само исключение, инициируемое методом pop, не будет соответство­ вать текущему уровню абстракции (статья 61).

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

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

354

С татья 64

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

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

после перехвата исключения ConcurrentModificationException нель­

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

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

355

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

Не игнорируйте исключений

Этот совет кажется очевидным, но он нарушается настолько ча­ сто, что заслуживает повторения. Когда разработчики API деклари­ руют, что некий метод инициирует исключение, этим они пытаются что-то вам сказать. Не игнорируйте это! Игнорировать исключения легко: необходимо всего лишь окружить вызов метода оператором try с пустым блоком catch:

//Пустой блок catch игнорирует исключение - крайне

//подозрительный код!

try {

} catch (SomeException е) {

}

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

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

356

С тать я 65

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

357

Г л а в а

ж д ж д

т т т т т ш м ш и и ж ж и м и ж м

■ № т т ж т ш ж щ я м !м ш ш ш ш 1 ш ш ш ш ш ш т т ! м ^ ^

Потоки

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

Синхронизируйте доступ потоков к совместно используемым изменяемым данным

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

358

С т а т ь я 66

состоянии, пока тот модифицируется другим потоком. С этой точки зрения, объект создается с согласованным состоянием (статья 13), а затем блокируется методами, имеющими к нему доступ. Эти ме­ тоды следят за состоянием объекта и (дополнительно) могут вызы­ вать для него переход состояния (state transition), переводя объект из одного согласованного состояния в другое. Правильное выполне­ ние синхронизации гарантирует, что ни один метод никогда не смо­ жет наблюдать этот объект в промежуточном состоянии.

Такая точка зрения верна, но не отражает всей картины.

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

Спецификация языка Java дает гарантию, что чтение и запись от­ дельной переменной, если это не переменная типа long или double, яв­ ляются атомарными операциями [JLS, 17.4.7]. Иными словами, га­ рантировано, что при чтении переменной (кроме long и double) будет возвращаться значение, которое было записано в эту переменную одним из потоков, даже если новые значения в эту переменную без какой-либо синхронизации одновременно записывают несколько потоков.

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

синхронизация необходима как для блокирования потоков, так и для надежного взаимодействия между ними. Это является следствием сугубо технического аспекта языка программирования Java, ко­ торый называется моделью памяти (memory model) [JL S , 17].

359

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