Effective Java tmprogramming Language GuideJ o s h u a b lo c h
Скачать 1.05 Mb.
|
Исключения Если исключения (exception) используются наилучшим образом, они способствуют написанию понятных, надежных и легко сопровождаемых программ. При неправильном применении результат может быть прямо противоположным. В этой главе даются рекомендации по эффективному использованию исключений. Используйте исключения лишь вис ключи тельных ситуациях bОднажды, если вам не повезет, вы сделаете ошибку в программе, например, такую // Неправильное использование исключений. Никогда так не делайте { int i = 0; while(true) а. f(); } catch(ArraylndexOutOfBoundsException е) { Что делает этот код Изучение кода не вносит полной ясности, и это достаточная причина, чтобы им не пользоваться. Здесь приведена плохо продуманная идиома для циклического перебора элементов в массиве. Когда производится попытка обращения к первому элементу за пределами массива, бесконечный цикл завершается инициированием исключительной ситуации ArraylndexOutOfBoundException, ее перехватом и последующим игнорированием. Предполагается, что это эквивалентно стандартной идиоме цикла по массиву, которую узнает любой программист java: for (int i = 0 ; i < a.length; i++) a[i].f(); 158 Но почему же кто-то выбрал идиому, использующую исключения, вместо другой, испытанной и правильной Это вводящая в заблуждение попытка улучшить производительность, которая исходит из ложного умозаключения, что, поскольку виртуальная машина проверяет границы при всех обращениях к массиву, обычная проверка на завершение цикла (i < a.length) избыточна и ее следует устранить. В этом рассуждении неверны три момента Так как исключения создавались для применения в исключительных условиях, лишь очень немногие реализации JVM пытаются их оптимизировать (если таковые есть вообще. Обычно создание, инициирование и перехват исключения дорого обходится системе • Размещение кода внутри блока try-catch препятствует выполнению определенных процедур оптимизации, которые в противном случае могли бы быть исполнены в современных реализациях jVM. Стандартная идиома цикла по массиву вовсе необязательно приводит к выполнению избыточных проверок, в процессе оптимизации некоторые современные реализации jVM отбрасывают их. Практически во всех современных реализациях jVM идиома, использующая исключения, работает гораздо медленнее стандартной идиомы. На моей машине идиома, использующая исключения, выполняет цикл от 0 до 99 в семьдесят раз медленней стандартной. Идиома цикла, использующая исключения, снижает производительность и делает непонятным программный код. Кроме того, нет гарантий, что она будет работать. При появлении непредусмотренной разработчиком ошибки указанная идиома может без предупреждений завершить работу, маскировав ошибку, и тем самым значительно усложнить процесс отладки. Предположим, вычисления в теле цикла содержат ошибку, которая приводит к выходу заграницы при доступе к какому-то совсем другому массиву. Если бы применялась правильная идиома цикла, эта ошибка породила бы необработанное исключение, которое вызвало бы немедленное завершение потока с соответствующим сообщением об ошибке. В случае же порочной, использующей исключения идиомы цикла исключение, вызванное ошибкой, будет перехвачено и неправильно интерпретировано как обычное завершение цикла. Мораль проста исключения, как и подразумевает их название, должны применяться лишь для исключительных ситуаций, при обычной обработке использовать их не следует никогда. Вообще говоря, вы всегда должны предпочитать стандартные, легко распознаваемые идиомы идиомам с ухищрениями, предлагающим лучшую производительность. Даже если имеет место реальный выигрыш в производительности, он может быть поглощен неуклонным совершенствованием реализаций jVM. А вот коварные ошибки и сложность поддержки, вызываемые чересчур хитроумными идиомами, наверняка останутся. 159 Этот принцип относится также к проектированию АР. Хорошо спроектированный API не должен заставлять своих клиентов использовать исключения для обычного управления потоком вычислений. Если в классе есть метод, зависящий от состояния (state-dependent), который может быть вызван лишь при выполнении определенных непредсказуемых условий, тов этом же классе, как правило, должен присутствовать отдельный метод, проверяющий состояние (state-testing), который показывает, можно ли вызывать первый метод. Например, класс Iterator имеет зависящий от состояния метод next, который возвращает элемент для следующего прохода цикла, а также соответствующий метод проверки состояния hasNext. Это позволяет применять для просмотра коллекции в цикле следующую стандартную идиому for (Iterator i = collection. iterator(); i. hasNext(); ) { Foo foo = (Foo) i.next(); } Если бы в классе Iterator не было бы метода hasNext, клиент был бы вынужден использовать следующую конструкцию // Не пользуйтесь этой отвратительной идиомой // для просмотра коллекции в цикле { Iterator i = collection.iterator(); while(true) { Foo foo = (Foo) i.next(); } } catch (NoSuchElementException е) { Этот пример также плох, как и пример с просмотром массива в цикле, приведенный вначале статьи. Идиома, использующая исключения, отличается от стандартной идиомы не только многословностью и запутанностью, но также худшей производительностью и способностью скрывать ошибки, возникающие в других, несвязанных с нею частях системы. В качестве альтернативы отдельному методу проверки состояния можно использовать особый зависящий от состояния метод он будет возвращать особое значение, например null, при вызове для объекта, имеющего неподходящее состояние. Для класса Iterator этот прием не годится, поскольку null является допустимым значением для метода next. Приведем некоторые рекомендации, которые помогут вам сделать выбор между методом проверки состояния и особым возвращаемым значением. Если к объекту возможен одновременный доступ без внешней синхронизации или если смена его СОСТОJШИЙ инициируется извне, может потребоваться прием с особым возвращаемым значением, поскольку состояние объекта может поменяться в период между вызовом метода, который проверяет состояние, и вызовом соответствующего метода, который зависит от состояния объекта. Особое возвращаемое значение может потребоваться для повышения производительности, когда метод проверки состояния, может при необходимости дублировать работу метода, зависящего от состояния объекта. Однако при прочих равных условиях метод проверки состояния предпочтительнее особого возвращаемого значения. При его использовании легче читать текст программы, а также проще обнаруживать и исправлять неправильное построение программы. Применяйте обрабатываемые исключения для восстановления, для программных ошибок используйте исключения времени выполнения bв языке программирования Java предусмотрены три типа объектов Throwable: обрабатываемые исключения (checked exception), исключения времени выполнения и exception) и ошибки (error). Программисты обычно путают, при каких условиях следует использовать каждый из этих типов. Решение не всегда очевидно, но есть несколько общих правил, в значительной мере упрощающих выбор. Основное правило при выборе между обрабатываемыми необрабатываемым исключениями гласит используйте обрабатываемые исключения для тех условий, когда есть основания полагать, что инициатор вызова способен их обработать. Генерируя обрабатываемое исключение, вы принуждаете инициатора вызова обрабатывать его в операторе catch или передавать дальше. Каждое обрабатываемое исключение, которое, согласно декларации, инициирует некий метод, является, таким образом, серьезным предупреждением для пользователя АР о том, что при вызове данного метода могут возникнуть соответствующие условия. Предоставляя пользователю АР обрабатываемое исключение, разработчик АР передает ему право осуществлять обработку соответствующего условия. Пользователь может пренебречь этим правом, перехватив исключение и проигнорировав его. Однако, как правило, это оказывается плохим решением (статья 47). Есть два типа необрабатываемых объектов T hrow able: исключения времени выполнения и ошибки. Поведение у них одинаковое ни тот, ни другой ненужно и, вообще говоря, нельзя перехватывать. Если программа инициирует необрабатываемое исключение или ошибку, то, как правило, это означает, что восстановление невозможно и дальнейшее выполнение программы принесет больше вреда, чем пользы. Если программа не перехватывает такой объект, его появление вызовет остановку текущего потока команд с соответствующим сообщением об ошибке. Используйте исключения времени выполнении для индикации программных ошибок. Подавляющее большинство исключений времени выполнения сообщает о нарушении предусловий violation). Нарушение предусловия означает лишь то, что клиент API не смог выполнить соглашения, заявленные в спецификации к этому API. Например, в соглашениях для доступа к массиву оговаривается, что индекс массива должен попадать в интервал от нуля до "длина массива минус один. Исключение ArrayIndexOutOfBounds указывает, что это предусловие было нарушено. Хотя в спецификации языка Java это не оговорено, существует строго соблюдаемое соглашение о том, что ошибки зарезервированы в JVM для того, чтобы фиксировать дефицит ресурсов, нарушение инвариантов и другие условия, делающие невозможным дальнейшее выполнение программы [C han98, H orstm anO O ]. Поскольку эти соглашения признаны практически повсеместно, лучше для Error вообще не создавать новых подклассов. Все реализуемые вами необрабатываемые исключения должны прямо или косвенно наследовать класс RuntimeException. Для исключительной ситуации можно определить класс, который не наследует классов Exception, RuntimeException и Error. В спецификации языка Java такие классы напрямую не оговариваются, однако неявно подразумевается, что они будут вести себя также, как обычные обрабатываемые исключения которые являются подклассами класса Exception, ноне. Когда же вы должны использовать этот класс Если одним словом, то никогда. Не имея никаких преимуществ перед обычным обрабатываемым исключением, он будет запутывать пользователей вашего API. Подведем итоги. для ситуаций, когда можно обработать ошибку и продолжить исполнение, используйте обрабатываемые исключения, для программных ошибок применяйте исключения времени выполнения. Разумеется, ситуация не всегда однозначна, как белое и черное. Рассмотрим случай с исчерпанием ресурсов, которое может быть вызвано программной ошибкой, например, размещением в памяти неоправданно большого массива, или настоящим дефицитом ресурсов. Если исчерпание ресурсов вызвано временным дефицитом или временным увеличением спроса, эти условия вполне могут быть изменены. Именно разработчик API принимает решение, возможно ли восстановление работоспособности программы в конкретном случае исчерпания ресурсов. Если высчитаете, что работоспособность можно восстановить, используйте обрабатываемое исключение. В противном случае применяйте исключение времени выполнения. Если неясно, возможно ли восстановление, то по причинам, описанным в статье 4 1 , лучше остановиться на необрабатываемом исключении. Разработчики API часто забывают, что исключения - это вполне законченные объекты, для КОТОРЫХ можно определять любые методы. Основное назначение таких методов - создание кода, который увязывал бы исключение с дополнительной информацией об условии, вызвавшем появление данной исключительной ситуации. Если таких методов нет, программистам придется разбираться со строковым представлением этого исключения, выуживая из него дополнительную информацию. Эта крайне плохая практика. Классы редко указывают какие-либо детали в своем строковом представлении, само строковое представление может меняться от реализации к реализации, от версии к версии. Следовательно, программный код, который анализирует строковое представление исключения, скорее всего окажется непереносимыми ненадежным. 162 Поскольку обрабатываемые исключения обычно указывают на ситуации, когда возможно продолжение выполнения, для такого типа исключений важно создать методы, которые предоставляли бы клиенту информацию, помогающую возобновить работу. Предположим, что обрабатываемое исключение инициируется при неудачной попытке позвонить с платного телефона из-за того, что клиент не предоставил достаточной суммы денег. Для этого исключения должен быть реализован метод доступа, который запрашивает недостающую сумму стем, чтобы можно было сообщить о ней пользователю телефонного аппарата. Избегайте ненужных обрабатываемых исключений исключения -' замечательная особенность языка программирования Java. В отличие от возвращаемых кодов, они заставляют программиста отслеживать условия возникновения исключений, что значительно повышает надежность приложения. Это означает, что злоупотребление обрабатываемыми исключениями может сделать API менее удобным для использования. Если метод инициирует одно или несколько обрабатываемых исключений, тов программном коде, из которого этот метод был вызван, должна присутствовать обработка этих исключений в виде одного или нескольких блоков catch, либо должно быть декларировано, что этот код сам инициирует исключения и передает их дальше. В любом случае перед программистом стоит нелегкая задача. Такое решение оправданно, если даже принадлежащем применении интерфейса API невозможно предотвратить возникновение условий для исключительной ситуации, однако программист, пользующийся данным API, столкнувшись с этим исключением, мог бы предпринять какие-либо полезные действия. Если не выполняются оба этих условия, лучше пользоваться необрабатываемым исключением. Роль лакмусовой бумажки в данном случае играет вопрос как программист будет обрабатывать исключение Является ли это решение лучшим } catch(TheCheckedException е) { throw new Error("Assertion error"); // Условие не выполнено. Этого не должно быть никогда А что скажете об этом } catch(TheCheckedException e) { e.printStackTrace(); Ладно, закончили работу. exit( 1); Если программист, применяющий API, не может сделать ничего лучшего, то больше подходит необрабатываемое исключение. Примером исключения, не выдерживающего подобной проверки, является CloneNotSupportedException. Оно инициируется 16 3 методом Object. сое, который должен использоваться лишь для объектов, реализующих интерфейс Cloneable (статья 10). Блок catch практически всегда соответствует невыполнению утверждения. Так что обрабатываемое исключение не дает программисту преимуществ, но требует от последнего дополнительных усилий и усложняет программу. Дополнительные действия со стороны программиста, связанные с обработкой обрабатываемого исключения, значительно увеличиваются, если это единственное исключение, инициируемое данным методом. Если есть другие исключения, метод будет стоять в блоке try, так что для этого исключения понадобится всего лишь еще один блок catch. Если же метод инициирует только одно обрабатываемое исключение, оно будет требовать, чтобы вызов соответствующего метода был помещен в блок try. В таких условиях имеет смысл подумать не существует ли какого-либо способа избежать обрабатываемого исключения. Один из приемов, позволяющих превратить обрабатываемое исключение в необрабатываемое, состоит враз биении метода, инициирующего исключение, на два метода, первый из которых будет возвращать булево значение, указывающее, будет ли инициироваться исключение. Таким образом, в результате преобразования АР последовательность вызова // Вызов с обрабатываемым исключением try { obj.action(args); catch(TheCheckedException е) { // Обработать исключительную ситуацию принимает следующий вид // Вызов с использованием метода проверки состояния // и необрабатываемого исключения if (obj.actionPermitted(args)) { obj.action(args); } else{ } // Обработать исключительную ситуацию Такое преобразование можно использовать не всегда, Если же оно допустимо, это может сделать работу с АР I более удобной. Хотя второй вариант последовательности вызова выглядит не лучше первого, полученный АР I имеет большую гибкость, В ситуации, когда программист знает, что вызов будет успешным, или согласен на завершение потока в случае неудачного вызова, преобразованный АР I позволяет использовать следующую упрощенную последовательность вызова obj,aotion(args): 164 Если вы предполагаете, что применение упрощенной последовательности вызова будет нормой, то описанное преобразование АР приемлемо. API, полученный в результате этого преобразования, в сущности, тот же самый, что и АР с методом "проверки состояния" (статья 39). Следовательно, к нему относятся те же самые предупреждения если к объекту одновременно и без внешней синхронизации могут иметь доступ сразу несколько потоков или этот объект может менять свое состояние по команде извне, указанное преобразование использовать не рекомендуется. Это связано стем, что в промежутке между вызовом actionPermitted и вызовом action состояние объекта может успеть поменяться. Если метод actionPermitted при необходимости и мог бы дублировать работу метода action, то от преобразования, вероятно, стоит отказаться по соображениям производительности. Предпочитайте стандартные исключения bОдной из сильных сторон экспертов, отличающих их от менее опытных программистов, является то, что эксперты борются за высокую степень повторного использования программного когда и обычно этого добиваются. Общее правило, гласящее, что повторно используемый код - это хорошо, относится и к технологии исключении. В библиотеках для платформы Java реализован основной набор необрабатываемых исключений, перекрывающий большую часть потребностей в исключениях для API. В этой статье обсуждаются наиболее часто применяемые исключения. Повторное использование уже имеющихся исключений имеет несколько преимуществ. Главное то, что они упрощают освоение и применение вашего API, поскольку соответствуют установленным соглашениям, с которыми программисты уже знакомы. С этим же связано второе преимущество, которое заключается в том, что программы, использующие ваш API, легче читать, поскольку там нет незнакомых, сбивающих столку исключений. Наконец, чем меньше классов исключений, тем меньше требуется места в памяти и времени на их загрузку. Чаще всего используется исключение IllegalArgumentException. Обычно оно инициируется, когда вызываемому методу передается аргумент с неправильным значением. Например, IllegalArgumentException может инициироваться в случае, если для параметра, указывающего количество повторов для некоей процедуры, передано Отрицательное значение. Другое часто используемое исключение - IllegalStateException. Оно обычно инициируется, если в соответствии с состоянием объекта вызов метода является неправомерным. Например, это исключение может инициироваться, 'Когда делается попытка использовать некий объект до его инициализации надлежащим образом. Вряд ли можно утверждать, что все неправильные вызовы методов сводятся к неправильным аргументам или неправильному состоянию, поскольку для определенных типов неправильных аргументов и состояний стандартно используются совсем другие 165 исключения. Если при вызове какому-либо параметру было передано null, тогда как значения null для него запрещены, тов этом случаев соответствии с соглашениями должно инициироваться исключение NullPointerException, а не IllegalArgumentException. Точно также, если параметру, который соответствует индексу некоей последовательности, при вызове было передано значение, выходящее заграницы допустимого диапазона, инициироваться должно исключение IndexOutOfBoundsException, а не IllegalArgumentException. Еще одно универсальное исключение, о котором необходимо знать ConcurrentModificationException. но должно инициироваться, когда объект, предназначавшийся для работы водном потоке или с внешней синхронизацией, обнаруживает, что его изменяют (или изменили) из параллельного потока. Последнее универсальное исключение, заслуживающее упоминания- UnsupportedOperationException. но инициируется, если объект не имеет поддержки производимой операции. По сравнению с другими исключениями, обсуждавшимися в этой статье, UnsupportedOperationException применяется довольно редко, поскольку большинство объектов обеспечивает поддержку всех реализуемых ими методов. Это исключение используется при такой реализации интерфейса, когда отсутствует поддержка одной или нескольких заявленных в нем дополнительных функций. Например, реализация интерфейса List, имеющая только функцию добавления элементов, будет инициировать это исключение, если кто-то попытается удалить элемент. В таблице 8.1 собраны самые распространенные из повторно используемых исключений. Часто используемые исключения |