Теория и практика Java: Дебаты об исключениях

Отмечать или не отмечать?

Главный совет по использованию исключений в языке Java заключается в том, чтобы отдавать предпочтение отмеченным исключениям при любом появлении исключений. Это предложение поддерживается как структурой языка (так как компилирующая программа заставляет Вас перечислить в сигнатуре метода все отмеченные исключения, которые могут появиться), так и ранними работами по стилю и использованию. Однако в последнее время некоторые известные авторы начали приходить к мнению, что неотмеченные исключения достойны находиться в хорошем классе Java в большей степени, нежели считалось до этого. В этой статье Брайан Гетц рассматривает все "за" и "против" использования неотмеченных исключений.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



08.02.2007

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

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

Расхожая мудрость

Общепринятая точка зрения относительно объявления исключения отмеченным или неотмеченным обобщается в следующем высказывании из "Руководства по Java" от Sun (см. раздел Ресурсы для получения более подробной информации):

Так как язык Java не требует методов для перехвата или задания исключения времени выполнения, то он является весьма привлекательным для программистов с точки зрения написания кода, который выдает только исключения времени выполнения или для того, чтобы все подклассы исключений наследовались от RuntimeException. Обе эти обходных манёвра позволяют программистам писать код Java, не думая о надоедливых ошибках компилятора и не задумываясь о необходимости задавать или перехватывать исключения. Хотя это и кажется удобным для программиста, тем не менее, этот метод не соответствует требованиям Java по заданию или перехвату, и может привести к появлению проблем у тех программистов, которые воспользуются Вашими классами.

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

Что же получится, если выдать RuntimeException или создать подкласс RuntimeException просто потому, что нет желания заниматься его описанием? Просто у Вас появится возможность выдавать исключение, без указания того, что Вы это делаете. Другими словами, это один из вариантов, как избежать документирования исключений, которые выдает метод. В каких случаях это хорошо? Когда же хорошо избегать документирования поведения метода? Ответ - "почти никогда".

Другими словами, Sun говорит, что нормой являются отмеченные исключения. В руководстве разными способами сообщается, что обычно Вы должны выдавать исключение и не выдавать RuntimeException - если только Вы не являетесь виртуальной машиной Java.

В своей книге Эффективное использование Java: Руководство по программированию (см. раздел Ресурсы), Джош Блох (Josh Bloch) предлагает следующие правила касательно отмеченных и неотмеченных исключений, они похожи на то, что предлагает "Руководство Java" (но менее жёсткие):

  • Пункт 39: Используйте исключения только в исключительных ситуациях. Это означает - не используйте исключения для управляющей логики, например, перехвата NoSuchElementException при вызове Iterator.next() вместо первоначальной проверки Iterator.hasNext().
  • Пункт 40: Используйте отмеченные исключения для восстановимых ситуаций, а динамические исключения для программных ошибок. В этом случае Блох согласен с идеей, предложенной Sun - динамические исключения должны использоваться только для индикации программных ошибок, например нарушений предусловий.
  • Пункт 41: Избегайте ненужного использования отмеченных исключений. Другими словами, не используйте отмеченные исключения в таких ситуациях, когда источник вызова может не восстановиться, или единственной предсказуемой реакцией программы является завершение ее работы.
  • Пункт 43: Выдавайте исключения, соответствующие абстракции. Другими словами, выдаваемые методом исключения должны быть определены на уровне абстракции в соответствии с тем, что делает метод, и не обязательно с описанием реализации на низком уровне. Например, метод, который загружает ресурсы из файлов, баз данных или JNDI, должен выдавать какое-нибудь исключение из ResourceNotFound, если он не может найти ресурс (обычно используется цепь исключений для сохранения первопричины), нежели исключения более низкого уровня IOException, SQLException или NamingException.

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

В последнее время несколько известных специалистов, в том числе Брюс Экель (Bruce Eckel) и Род Джонсон (Rod Johnson) открыто заявили, что, хотя они первоначально были согласны с ортодоксальной позицией относительно отмеченных исключений, они пришли к выводу, что привилегированное использование отмеченных исключений является не такой уж хорошей идеей, как показалось на первый взгляд, и что отмеченные исключения стали источником проблем во многих больших проектах. Экель придерживается оригинального подхода, предлагающего сделать все исключения неотмеченными, а точка зрения Джонсона более консервативна, но всё же он считает, что традиционное предпочтение отмеченных исключений является избыточным. (Важно отметить, что разработчики архитектуры C#, у которой, наверняка, был большой опыт использования технологий Java, предпочли не использовать отмеченные исключения в проекте языке, оставив неотмеченными все исключения. Однако позднее они оставили места и для разработки отмеченных исключений.)


Немного критики отмеченных исключений

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

Отмеченные исключения некстати раскрывают детали реализации

Сколько раз Вам приходилось видеть (или писать) метод, который выдает SQLException или IOException, даже если оказывалось, что он не имеет ничего общего с базами данных или файлами? Для разработчика достаточно типично просто подсчитать все исключения, которые могут быть выданы в первой реализации метода и добавить их к конструкции throws метода (многие IDE даже помогут вам осуществить эту задачу). Проблема сквозного подхода заключается в том, что он нарушает Пункт 43, описанный Блохом - выданные исключения находятся на уровне абстракции, что противоречит методу, выдающему их.

Метод, задача которого состоит в том, чтобы загружать профиль пользователя, должен выдать NoSuchUserException, если он не может найти пользователя, а не SQLException - вызывающий оператор может предполагать вероятность того, он не найдет пользователя, но ему непонятно, что делать с SQLException. Цепочка исключений может быть использована для того, чтобы выдать более подходящее исключение без деталей (например, трассировки стека) лежащей в его основе ошибки, позволяя абстрактным уровням оградить вышестоящие уровни от деталей нижестоящих уровней, сохранив ту информацию, которая может быть полезна для отладки.

При этом такие пакеты как JDBC как будто специально разработаны так, чтобы усложнить избежание данной проблемы. Каждый метод в интерфейсе JDBC выдает SQLException, но во время доступа к БД могут возникать различные проблемы, а разные методы могут быть подвержены различным видам ошибок. SQLException может отражать проблему на уровне системы (невозможно установить соединение с базой данных), логическую проблему (больше нет строк в списке результатов), или проблему, касающуюся конкретных данных (первичный ключ строки, который Вы только что пытались ввести, уже существует или нарушает целостность сущностей-объектов). Вызывающие операторы не различают эти типы исключений SQLException, не попытавшись выполнить синтаксический разбор текста сообщения. (SQLException предоставляет методы извлечения кода типичных для базы данных ошибок и параметров состояния SQL, но на практике редко используется для различения ошибок баз данных.)

Неустойчивые сигнатуры метода

Проблема неустойчивых сигнатур метода связана с предыдущей проблемой - если вы просто передаёте исключения через метод, то каждый раз, когда Вы изменяете реализацию метода, Вам приходится изменять и сигнатуру метода, а также изменять весь код, который его вызывает. Организация метода сигнатур становится дорогой задачей, как только класс поступает в производство. Однако в соответствии с Пунктом 43 Блоха, данная проблема является еще одним симптомом ошибки. Метод должен выдать исключение, когда он столкнётся с ошибкой, но это исключение должно отражать то, что делает метод, а не как он это делает.

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

Нечитаемый код

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

Поглощение исключений

Мы видели код, в котором исключение перехватывается, но внутри блока catch нет кода. Хотя данный прием программирования, безусловно, является плохим, легко понять, откуда он возникает - во время разработки упрощённой версии кто-нибудь "упаковывает" код блоком try...catch, а затем просто забывает вернуться к нему и заполнить блок catch. Хотя эта ошибка и является распространённой, от нее очень легко избавиться - редакторы, компиляторы или статические контрольные инструменты легко находят и выдают предупреждение о том, что исключение пропущено.

Слишком обобщенный блок try...catch - это еще одна форма поглощения исключения, которую сложнее обнаружить, так как она является результатом (спорной) структуры иерархии классов исключений в библиотеке классов Java. Давайте рассмотрим метод, который выдает четыре типа исключений, а когда вызывающий оператор обнаруживает любые из них, то он собирается перехватить их, зарегистрировать их и возвратить. Один из способов реализации данной стратегии связан с блоком try...catch с четырьмя компонентами catch - по одному на каждый тип исключения. Во избежание проблемы нечитаемости кода некоторые разработчики заново разлагают этот код, как показано в листинге 1:

Листинг 1. Случайное поглощение RuntimeException
try { 
  doSomething();
}
catch (Exception e) { 
  log(e);
}

Хотя этот код более компактен, чем четыре блока catch, все равно возникает проблема - он тоже перехватывает все RuntimeException, которые могли быть выданы doSomething и предотвращает их распространение.

Слишком много возвращений исключений

Если исключение генерируется в функции низкого уровня, и проходит через большое число уровней кода, то оно может быть перехвачено, возвращено и повторно выдано несколько раз прежде, чем оно, наконец, будет обработано. Когда, в конечном счете, исключение зарегистрировано, трассировка стека может занимать несколько страниц, так как трассировка стека будет повторяться несколько раз - по одному на каждое возвращение. (Реализация цепочки исключений в JDK версии 1.4 и более поздних версиях несколько уменьшает эту проблему.)


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

Брюс Экель, автор работы Думая на языке Java (см. раздел Ресурсы) говорит, что спустя годы использования языка Java, он пришёл к выводу, что отмеченные исключения были ошибкой - экспериментом, который стоит признать неудачным. Экель выступает за то, чтобы все исключения оставались неотмеченными, и предлагает класс, показанный в Листинге 2, как способ превращения отмеченных исключений в неотмеченные, при сохранении способности перехватывать особые типы исключений, когда они проходят через стек (см. его статью в разделе Ресурсы, в которой приведено описание применения):

Листинг 2. Класс адаптера исключений Экеля
			class ExceptionAdapter extends RuntimeException {
  private final String stackTrace;
  public Exception originalException;
  public ExceptionAdapter(Exception e) {
    super(e.toString());
    originalException = e;
    StringWriter sw = new StringWriter();
    e.printStackTrace(new PrintWriter(sw));
    stackTrace = sw.toString();
  }
  public void printStackTrace() { 
    printStackTrace(System.err);
  }
  public void printStackTrace(java.io.PrintStream s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void printStackTrace(java.io.PrintWriter s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void rethrow() { throw originalException; }
}

Если Вы познакомитесь с обсуждениями, приведенными на сайте Экеля, то обнаружите, что точки зрения участников различаются. Одни считают его предложение нелепым, в то время как другие находят его отличной идеей. (По моему мнению, хотя правильное использование исключений, несомненно, связано с рядом проблем, и отрицательных примеров использования исключений становится всё больше, но те люди, которые согласны с автором, неправы, так же как и тот политик, который выступит за субсидирование приобретения шоколада, и получит огромную поддержку среди 10-летних избирателей.)

Род Джонсон, автор книги Проектирование и разработка J2EE (см. Ресурсы), которая является одной из лучших когда-либо прочитанных мною книг по разработке Java, J2EE, придерживается менее радикальной точки зрения. Он перечисляет несколько категорий исключений и определяет стратегию для каждой. Некоторые исключения являются вторичными кодами возврата (которые, в общем, указывают на нарушение бизнес-правил), а некоторые относятся к разряду "что-то пошло не так" (например, невозможность установить соединение с базой данных). Джонсон предлагает использовать отмеченные исключения для первой категории (альтернативные коды возврата), и исключения времени выполнения для второй категории. В категории "что-то пошло не так" мотивацией является простое понимание того факта, что ни один вызывающий оператор не будет эффективно обрабатывать такое исключение, поэтому оно может проходить через стек с минимумом воздействия на промежуточный код (и минимизировать возможность поглощения исключения).

Джонсон также указывает второй план, для которого он задаёт вопрос "Только ли меньшинство вызывающих операторов столкнётся с этой проблемой?" Для таких сценариев он также предлагает использовать неотмеченных приложений. В качестве примера этой категории он перечисляет исключения JDO - чаще всего исключения JDO указывают на условие, с которым вызывающий оператор работать не будет, но иногда возникают такие случаи, в которых определенные виды исключений могут быть перехвачены и обработаны. Вместо того, чтобы заставлять оставшиеся классы JDO платить за возможность перехвата и повторной выдачи данных исключений, он предлагает использовать неотмеченных исключений.

Использование неотмеченных исключений

Решение об использовании неотмеченных исключений является достаточно сложным, и понятно, что здесь не может быть однозначного ответа. Sun советует не использовать их вообще, а C#-подход (с которым согласны Экель и другие) рекомендует использовать их везде. Третьи напоминают о существовании золотой середины.

После использования исключений в C++, где все исключения являются неотмеченными, я обнаружил, что самый большой риск при работе с неотмеченными исключениями состоит в том, что они не документируются самостоятельно, как это делают отмеченные исключения. Хотя разработчик API явно документирует все выданные исключения, у вызывающих операторов просто нет возможности узнать о том, какие исключения были перехвачены в их коде. К сожалению, мой опыт показывает, что большинство API C++ очень плохо задокументированы, а даже те, которые задокументированы, часто не обладают нужной информацией, о том, какие исключения могут быть выданы из данного метода. Я не вижу причин, почему эта проблема не будет аналогична библиотекам классов Java, которые в значительной степени полагаются на неотмеченные исключения. Достаточно сложно полагаться на Ваши собственные способности в программировании и способности Ваших коллег; страшно полагаться на способность документировать того, чей код вы использовали шестнадцать фреймов ниже Вашего стека вызовов в качестве вашего главного механизма обработки ошибок.

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


Документировать, документировать, документировать

Если вы решили использовать неотмеченные исключения, то Вам необходимо тщательно документировать свой выбор, включая документирование в Javadoc всех неотмеченных исключений, которые могут быть выданы методом. Джонсон предлагает сделать выбор между отмеченными и неотмеченными исключениями, основываясь на используемых пакетах. При использовании неотмеченных исключений помните также о том, что Вам может понадобиться использование заблокированного try...finally, даже если Вы не перехватываете исключения, для того чтобы Вы могли выполнить очистку, например, закрыть связь с базой данных. При работе с отмеченными приложениями, try...catch служит напоминанием о необходимости добавить конструкцию finally. При работе с неотмеченными исключениями, у нас нет такой опоры, на которую можно положиться.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=195789
ArticleTitle=Теория и практика Java: Дебаты об исключениях
publish-date=02082007