Effective Java tmprogramming Language GuideJ o s h u a b lo c h
Скачать 1.05 Mb.
|
Исключение Повод для использования IllegalArgumentException Неправильное значение параметра IllegalStateException Состояние объекта неприемлемо для вызова метода Значение параметра равно null, а это запрещено IndexOutOfBoundsException Значение параметра, задающего индекс, выходит за пределы диапазона ConcurrentModificationException Обнаружена параллельная модификация объекта из разных потоков, а это запрещено Объект не имеет поддержки указанного метода Помимо перечисленных исключений, при определенных обстоятельствах могут применяться и другие исключения. Например, при реализации таких арифметических объектов, как комплексные числа и матрицы, уместно пользоваться исключениями ArithmeticException и NumberFormatException. Если исключение отвечает вашим 166 потребностям - пользуйтесь им, но только чтобы условия, при которых выбудете его инициировать, не вступали в противоречие с документацией к этому исключению. Выбирая исключение, следует исходить из его семантики, а не только из названия. Кроме того, если вы хотите дополнить имеющееся исключение информацией об отказе (статья 45), не стесняйтесь создавать для него подклассы. И наконец, учитывайте, что выбор исключения - не всегда точная наука, поскольку "поводы для использования, приведенные в таблице 8.1, не являются взаимоисключающими. Рассмотрим, например, объект, соответствующий колоде карт. Предположим, что для него есть метод, осуществляющий выдачу карт из колоды, причем в качестве аргумента ему передается количество требуемых карт. Допустим, что при вызове с этим параметром было передано значение, превышающее количество карт, оставшихся в колоде. Эту ситуацию можно толковать как IllegalArgumentException значение параметра "размер сдачи" слишком велико) либо как IllegalStateException объект "колода" содержит слишком мало карт для обработки запроса. В данном случае, по-видимому, следует использовать IllegalArgumentException, но непреложных правил здесь не существует. Инициируйте исключения, соответствующие абстракции Если метод инициирует исключение, не имеющее видимой связи с решаемой задачей, это сбивает столку. Часто это происходит, когда метод передает исключение, инициированное абстракцией нижнего уровня. Это не только приводит в замешательство, но и засоряет интерфейс верхнего уровня деталями реализации. Если в следующей версии реализация верхнего уровня поменяется, то также могут поменяться и инициируемые им исключения, в результате чего могут перестать работать имеющиеся клиентские программы. Во избежание этой проблемы верхние уровни приложения должны перехватывать исключения нижних уровней ив свою очередь, инициировать исключения, которые можно объяснить в терминах абстракции верхнего уровня. Описываемая идиома, которую мы называем трансляцией исключении. (exception translation), выглядит следующим образом // Трансляция исключения { // Использование абстракции нижнего уровня // для выполнения наших указаний } catch(LowerLevelException е) { throw new HigherLevelException( ... ); } 167 Приведем конкретный пример трансляции исключения, взятый из класса AbstractSequentialList, который представляет собой скелетную реализацию статья 16) интерфейса List. В этом примере трансляция исключения продиктована спецификацией метода get в интерфейсе List: /** * Возвращает элемент, находящийся в указанной позиции * в заданном списке. * @throws IndexOutOfBoundsException, если индекс находится * за пределами диапазона (index < 0 II index > = size()). */ public Object get(int index) { ListIterator i = listItеrаtor(index); try { return i. next(); catch (NoSuchElementException е) { throw new IndexOutOfBoundsException("Index: " + index); } В тех случаях, когда исключение нижнего уровня может быть полезно при анализе ситуации, вызвавшей исключение, лучше использовать особый вид трансляции исключений, называемый сцеплением исключении (exception chaining). При этом исключение нижнего уровня передается с исключением верхнего уровняв последнем создается открытый метод доступа, позволяющий извлечь исключение нижнего уровня // Сцепление исключений try { // Использование абстракции нижнего уровня // для выполнения наших указаний } catch(LowerLevelException е) { throw new H1gherLevelException(e); в версии 1.4 сцепление исключений поддерживается классом Throwable. В этой и последующих версиях можно использовать преимущества такой поддержки, связав ваш конструктор исключения верхнего уровня с конструктором Throwable(Throwable): // Сцепление исключений в версии 1.4 HigherLevelException(Throwable t) { super(t); } 168 Если же вы используете более раннюю версию, ваше исключение должно само сохранять исключение нижнего уровня и предоставлять соответствующий метод доступа // Сцепление исключений в версии, предшествующей 1.4 private Throwable cause; НighегLеvеlЕхсерtiоп(Тhгоwаblе t) { cause = t; } public Throwable getCause() { return cause; Дав методу доступа название getCause и применив указанную декларацию, вы получите гарантию того, что ваше исключение будет взаимодействовать с механизмом сцепления исключений для данной платформы так, как если бы вы использовали исключение в версии 1.4. Это дает то преимущество, что в исключение верхнего уровня стандартным образом будет интегрирована трассировка стека для исключения нижнего уровня. Кроме того, это позволяет задействовать стандартные средства отладки для доступа к исключению нижнего уровня. Хотя трансляция исключений лучше, чем бессмысленная передача исключений с нижних уровней, злоупотреблять ею не следует. Самый хороши" способ обработки исключений нижнего уровня - полностью исключить их возможность. Для этого перед выбором метода нижнего уровня необходимо убедиться в том, что он будет выполнен успешно, иногда добиться этого можно путем явной проверки аргументов метода верхнего уровня перед их передачей на нижний уровень. Если предупредить появление исключений на нижних уровнях невозможно, то лучшее решение состоит в том, чтобы верхний уровень молча обрабатывал эти исключения, изолируя клиента от проблем нижнего уровня. В таких условиях чаще всего достаточно протоколировать исключения, используя какой-либо механизм регистрации, например java,util,logging, появившийся в версии 1.4. Это дает возможность администратору исследовать возникшую проблему ив тоже время изолирует от нее программный код клиента и конечного пользователя. В ситуациях, когда невозможно предотвратить возникновение исключений на нижних уровнях или изолировать от них верхние уровни, как правило, должен применяться механизм трансляции исключений. Непосредственную передачу исключений с нижележащего уровня на верхний следует разрешать только тогда, когда, исходя из описания метода на нижнем уровне, можно дать гарантию, что все инициируемые им исключения будут приемлемы для абстракции верхнего уровня. 169 Для каждого метода документируйте все инициируемые исключения bОписание инициируемых методом исключений составляет важную часть документации, которая необходима для правильного применения метода. Поэтому крайне важно, чтобы вы уделили время тщательному описанию всех исключений, инициируемых каждым методом. Обрабатываемые исключения всегда декларируйте по отдельности с помощью тега @throw s (Javadoc), четко описывайте условия, при которых каждое из них инициируется. Не пытайтесь сократить описание, объявляя о том, что метод инициирует некий суперкласс исключений, вместо того, чтобы декларировать несколько классов возможных исключений. Например, никогда не объявляйте, что метод инициирует исключение Exception или, что еще хуже, исключение Throw able. Помимо того, что такая формулировка не дает программисту никакой информации о том, какие исключения могут быть инициированы данным методом, она значительно затрудняет работу с методом, поскольку надежно перекрывает любое другое исключение, которое может быть инициировано в этом же месте. Хотя язык Java не требует, чтобы программисты декларировали необрабатываемые исключения, которые могут быть инициированы данным методом, имеет смысл документировать их столь же тщательно, как и обрабатываемые исключения. Необрабатываемые исключения обычно представляют ошибки программирования (статья 40), ознакомление программиста со всеми этими ошибками может помочь ему избежать их. Хорошо составленный перечень необрабатываемых исключений, которые может инициировать метод, фактически описывает предусловия для его успешного выполнения. Важно, чтобы в документации к каждому методу были описаны его предусловия, а описание необрабатываемых исключений как рази является наилучшим способом выполнения этого требования. Особенно важно, чтобы для методов интерфейса были описаны необрабатываемые исключения, которые могут быть ими инициированы. Такая документация 'является частью основных соглашениu для интерфейса и обеспечивает единообразное поведение различных его реализаций. Для описания каждого необрабатываемого исключения, которое может быть инициировано методом, используйте тег @throw s (Javadoc), однако ненужно с помощью ключевого слова throw s включать необрабатываемые исключения в декларацию метода. Программист, пользующийся вашим API, должен знать, какие из исключений обрабатываются, а какие - нет, поскольку в первом и втором случаях на него возлагается различная ответственность. Наличие описания, соответствующего тегу @ throw s, и отсутствие заголовка к методу, соответствующего декларации throw s, создает мощный визуальный сигнал, помогающий программисту отличить обрабатываемые исключения от необрабатываемых. 170 Следует отметить, что документирование всех необрабатываемых исключений, которые могут быть инициированы каждым методом- это идеал, который не всегда достижим в реальности. Когда производится пересмотр класса и предоставляемый пользователю метод меняется так, что начинает инициировать новые необрабатываемые исключения' это не является нарушением совместимости ни на уровне исходных текстов, ни на уровне байт-кода. Предположим, некий класс вызывает метод из другого класса, написанного независимо. Авторы первого класса могут тщательно документировать все необрабатываемые исключения, инициируемые каждым методом. Однако если второй класс был изменен так, что теперь он инициирует дополнительные необрабатываемые исключения, первый класс не претерпевший изменений) тоже будет передавать эти новые необрабатываемые исключения, хотя он их и не декларировал. Если одно и тоже исключение по одной и тоже причине инициируется несколькими методами, его описание можно поместить в общий комментарий к документации для всего класса, а не описывать его отдельно для каждого метода. Примером такого рода является исключение Прекрасно было бы в комментарии к классу сказать "все методы этого класса инициируют исключение NullPointerException, если с каким-либо параметром была передана нулевая ссылка на объект" или другие слова стем же смыслом. Вопи сани е исключения добавляйте информацию о сбое Если выполнение программы завершается аварийно из-за необработанного исключения, система автоматически распечатывает трассировку стека для этого исключения. Трассировка стека содержит строковое представление данного исключения, результат вызова его метода toString . Обычно это представление состоит из названия класса исключения и описания исключения (detail message). Часто это единственная информация, с которой приходится иметь дело программистам или специалистам по наладке, исследующим сбой программы. И если воспроизвести этот сбой нелегко, то получить какую- либо еще информацию будет трудно или даже вообще невозможно. Поэтому крайне важно, чтобы метод toString в классе исключения возвращал как можно больше информации о причинах отказа. Иными словами, строковое представление исключения должно зафиксировать отказ для последующего анализа. Для фиксации сбоя строковое представление исключения должно содержать значения всех параметров и полей, "способствовавших появлению этого исключения. Например, описание исключения IndexOutOfBounds должно содержать нижнюю границу, верхнюю границу и действительный индекс, который не уложился в эти границы. Такая информация говорит об отказе очень многое. Любое из трех значений или все они вместе могут быть неправильными. Представленный индекс может оказаться на единицу меньше нижней границы или быть равен верхней границе (ошибка 1 7 1 границы" - fencepost error) либо может иметь несуразное значение, как слишком маленькое, таки слишком большое. Нижняя граница может быть больше верхней (серьезная ошибка нарушения внутреннего инварианта. Каждая из этих ситуаций указывает на свою проблему, и если программист знает, какого рода ошибку следует искать, это в огромной степени облегчает диагностику. Хотя добавление в строковое представление исключения всех относящихся к делу "достоверных данных" является критическим, обычно нет надобности в том, чтобы оно было пространным. Трассировка стека, которая должна анализироваться вместе с исходными файлами приложения, как правило, содержит название файла и номер строки, где это исключение возникло, а также файлы и номера строк из стека, соответствующие всем остальным вызовам. Многословные пространные описания сбоя, как правило, излишни - необходимую информацию можно собрать, читая исходный текст программы. Не, следует путать строковое представление исключения и сообщение об ошибке на пользовательском уровне, которое должно быть понятно конечным пользователям. В отличие от сообщения об ошибке, описание исключения нужно главным образом программистами специалистам по наладке для анализа причин сбоя. Поэтому содержащаяся в строковом представлении информация гораздо важней его вразумительности. Один из приемов, гарантирующих, что строковое представление исключения будет содержать информацию, достаточную для Описания сбоя, состоит в том, чтобы эта информация запрашивалась в конструкторах исключения, а в строке описания. Само же описание исключения можно затем генерировать автоматически для представления этой информации. Например, вместо конструктора String исключение I ndexOutOfBounds могло бы иметь следующий конструктор /** * Конструируем IndexOutOfBoundsException * @раram lowerBound – самое меньшее из разрешенных значений индекса * @param upperBound – самое большее из разрешенных значений индекса плюс один @раrаm index – действительное значение индекса Public IndexOutOfBoundsExoeption(int lowerBound, int index) { // Генерируем описание исключения, // фиксирующее обстоятельства отказа super( "Lower bound: " + lowerBound + “,Upper bound: " + uppe rBound + “,Index: " + index); } 172 К сожалению, хотя ее очень рекомендуют, эта идиома в библиотеках для платформы Java используется не слишком интенсивно. С ее помощью программист, инициирующий исключение, может с легкостью зафиксировать обстоятельства сбоя Вместо того чтобы заставлять каждого пользующегося классом генерировать свое строковое представление, в этой идиоме собран фактически весь код, необходимый для того, чтобы 'качественное строковое представление генерировал сам класс исключения. Как отмечалось в статье 40, возможно, имеет смысл, чтобы исключение предоставляло методы доступа к информации об обстоятельствах сбоя (в представленном выше примере это lowerBound, upperBound и I ndex). Наличие таких методов доступа для обрабатываемых исключений еще важнее, чем для необрабатываемых, поскольку информация об обстоятельствах сбоя может быть полезна для восстановления работоспособности программы. Программный доступ к деталям необрабатываемого исключения редко интересует программистов (хотя это и не исключено. Однако, согласно общему принципу (статья 9), такие методы доступа имеет смысл создавать даже для необрабатываемых исключений. Добивайтесь атомарно ст им е то до в по отношению к сбоям После того как объект инициирует исключение, обычно необходимо, Ч1'обы он оставался во вполне определенном, пригодном для дальнейшей обработки состоянии, даже несмотря на то, что сбой произошел непосредственно в процессе ВЫl1Dлнения -операции. Особенно это касается обрабатываемых исключений, когда предполагается, что клиент будет восстанавливать работоспособность программы. Вообще говоря, вызов метода, завершившийся сбоем, должен оставлять обрабатываемый объект в том же состоянии, в каком тот был перед вызовом. Метод, обладающий таким свойством, называют атомарным по отношению к сбою (failure atomic). Добиться такого эффекта можно несколькими способами. Простейший способ заключается в создании неизменяемых объектов (статья 13). Если объект неизменяемый, получение атомарности не требует усилий. Если операция заканчивается сбоем, это может помешать созданию нового объекта, но никогда не оставит уже имеющийся объект в неопределенном состоянии, поскольку состояние каждого неизменяемого объекта согласуется в момент его создания и после этого уже не меняется. для методов, работающих с изменяемыми объектами, атомарность по отношению к сбою чаще всего достигается путем проверки правильности параметров перед выполнением операции (статья 23). Благодаря этому, любое исключение будет инициироваться до того, как начнется модификация объекта. В качестве примера рассмотрим метод Staok.рор из статьи 5: public Objeot рор() { if (size == 0) throw new EmptyStaokExoeption(); 173 Object result = elements[-size]; elements[size] = null; // Убираем устаревшую ссылку return Если убрать начальную проверку размера, метод все равно будет инициировать исключение при попытке получить элемент из пустого стека. Однако при этом он будет оставлять поле size в неопределенном (отрицательном) состоянии. А это приведет к тому, что сбоем будет завершаться вызов любого метода в этом объекте. Кроме того, само исключение, инициируемое методом рор, не будет соответствовать текущему уровню абстракции (статья 43). Другой прием, который тесно связан с предыдущими позволяет добиться атомарности по отношению к сбоям, заключается в упорядочении вычислений таким образом, чтобы все фрагменты кода, способные повлечь сбой, предшествовали первому фрагменту, который модифицирует объект. Такой прием является естественным расширением предыдущего в случаях, когда невозможно произвести проверку аргументов, не выполнив хотя бы части вычислений. Например, рассмотрим случай с классом ТгееМар, элементы которого сортируются по некоему правилу. Для того чтобы в экземпляр ТгееМар можно было добавить элемент, последний должен иметь такой тип, который допускал бы сравнение с помощью процедур, обеспечивающих упорядочение ТгееМар. Попытка добавить элемент неправильного типа, естественно, закончится сбоем (и исключением ClassCastException), который произойдет в процессе поиска этого элемента в дереве, но до того, как в этом дереве что-либо будет изменено. Третий, редко встречающийся прием, заключается в написании специального кода восстановления (recovery code), который перехватывает сбой, возникающий входе выполнения операции, и заставляет объект вернуться в то состояние, в котором он находился в момент, предшествующей началу операции. Этот прием используется главным образом для структур, записываемых в базу данных. Наконец, последний прием, позволяющий добиться атомарности метода, заключается в том, чтобы выполнять операцию на временной копии объекта, и как только операция будет завершена, замещать содержимое объекта содержимым его временной копии. Такой прием подходит для случая, когда вычисления могут быть выполнены намного быстрее, если поместить данные во временную структуру. Например, метод Collections.sort перед выполнением сортировки загружает полученный список в некий массив стем, чтобы облегчить доступ к элементам вовремя внутреннего цикла сортировки. Это сделано для повышения производительности, однако имеет и другое дополнительное преимущество - гарантию того, что предоставленный методу список останется нетронутым, если процедура сортировки завершится сбоем. К сожалению, не всегда можно достичь атомарности по отношению к отказам. Например, если два потока одновременно, без должной синхронизации пытаются модифицировать некий объект, последний может остаться в неопределенном состоянии. А потому после перехвата исключения ConcurrentModificationException нельзя 174 полагаться на то, что объект все еще пригоден к использованию. Ошибки (в отличие от исключений, как правило, невосстановимы, и потому методам ненужно даже пытаться сохранять атомарность в случае появления ошибки. Даже там, где можно получить атомарность по отношению к сбоям, она не всегда желательна. Для некоторых операций она существенно увеличивает затраты ресурсов и Сложность вычислений. Вместе стем очень часто это свойство достигается без особого Труда, если хорошо разобраться с проблемой. Как правило, любое исключение, добавленное в спецификацию метода, должно оставлять объект в том состоянии, в котором он находился до вызова метода. В случае нарушения этого правила в документации Должно быть четко указано, в каком состоянии будет оставлен объект. К сожалению, множество имеющейся документации к API не стремится достичь этого идеала. Не игнорируйте исключений bЭтот совет кажется очевидным, но он нарушается настолько часто, что заслуживает повторения. Когда разработчики API декларируют, что некий метод инициирует исключение, этим они пытаются что-то вам скliзать. Не игнорируйте это Игнорировать исключения легко необходимо всего лишь Окружить вызов метода оператором try с пустым блоком catch: |