[an error occurred while processing this directive] IT • archiv :: Print

IT • archiv


[an error occurred while processing this directive]

[an error occurred while processing this directive]

Исключения в Java: ничего исключительного

[an error occurred while processing this directive](none) [an error occurred while processing this directive](none)[an error occurred while processing this directive] ::
[an error occurred while processing this directive](none)
[an error occurred while processing this directive]([an error occurred while processing this directive] Гурав Пал, Сонал Банзал [an error occurred while processing this directive])

[an error occurred while processing this directive](none)

Обработка особых ситуаций в Java от начала до конца.

Exception-Handling Strategies
PDF versionPDF версия
Обзор
Разумное и надлежащее использование механизма обработки особых ситуаций Java может приносить дивиденты, приводя к хорошо работающему коду. Чтобы использовать мощные режимы обработки ошибок в Java, пользователи должны понимать ключевые вопросы, влияющие на дизайн и реализацию. Эта статья помогает в разработке правильной и последовательной стратегии обработки ошибок. (2000 слов)

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

Чтобы разработать очевидную и последовательную стратегию обработки исключений, ответьте на вопросы, непрерывно досаждающие Java разработчикам:

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

Давайте углубимся в различные аспекты исключений.

Какие исключения я должен использовать?

Исключения бывают двух типов:

  1. Исключения, вызванные компилятором, или проверенные исключения
  2. Исключения времени выполнения или непроверенные исключения

Исключения, вызванные компилятором (проверенные) являются экземплярами класса Exception или одного из его подклассов — исключая ветвь RuntimeException. Компилятор ожидает, что все проверенные исключения будут соответствующим образом обработаны. Проверенные исключения должны быть объявлены в throws разделе метода, который может их вызвать — конечно, если они не будут обработаны в этом же методе. Вызывающий метод должен позаботится об этих исключениях путем их обработки или объявлении их в секции throws. Таким образом, проверенные исключения дают программисту возможность их обработки.

Например проверенным исключением является java.io.IOException. Исходя из его названия, оно вызывается при неправильном завершении операции ввода/вывода. Посмотрите на следующий код:

try
 {
    BufferedReader br = 
  new BufferedReader(new FileReader("MyFile.txt"));
    String line = br.readLine();
 }
 catch(FileNotFoundException fnfe)
 {
    System.out.println(
 	"Файл MyFile.txt не найден.");
 }
 catch(IOException ioe)
 {
    System.out.println(
 	"Не могу читать из файла MyFile.txt");
 }

Конструктор класса FileReader вызывает исключения FileNotFoundException — подкласс класса IOException — если файл не найден. Иначе, если файл существует, но по каким-то причинам метод readLine() не может из него читать, FileReader вызывает IOException.

Исключения времени выполнения (непроверенные) — это экземпляры класса RuntimeException или одного из его подклассов. Вы не должны объявлять такие исключения в секции throws вызываемого метода. Также, вызывающий метод не должен их обрабатывать, хотя и может. Непроверенные исключения обычно вызываются только при возникновении проблем в окружении виртуальной машины Java (JVM). Также, программисты должны воздерживаться от их обработки, так как JVM удобнее самой обработать эту ситуацию.

java.lang.ArithmeticException — это пример непроверенного исключения, вызываемого при возникновении арифметических ошибок. Например, деление на ноль вызывает экземпляр этого класса. Следующий код иллюстрирует использование непроверенного исключения :

public static float fussyDivide(float dividend, float divisor) throws
 FussyDivideException
 {
    float q;
    try
    {
     q = dividend/divisor;
    }
    catch(ArithmeticException ae)
    {
       throw new FussyDivideException(
 	"Can't divide by zero.");
    }
 }
 public class FussyDivideException extends Exception
 {
    public FussyDivideException(String s)
    {
       super(s);
    }
 }

fussyDivide() гарантирует вызывающему методу, что вызываемый не допустит деления на ноль. Он делает это путем перехвата ArithmeticException — непроверенного исключения — и за этим вызывает FussyDivideException — проверенное исключение.

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

Когда я должен использовать исключения?

Спецификация Java гласит, что "исключение должно быть вызвано при нарущении семантических ограничений," что в основном подразумевает вызов исключения в невозможных в обычных условиях ситуациях или при грубом нарушении допустимого поведения. (Смотри в Ресурсах Спецификацию языка Java, написанную James Gosling, Bill Joy, и Guy Steele.)

Чтобы более ясно понять виды поведения, которые могут классифицироваться как "нормальное" или исключительное, смотрите на примеры кода

Случай 1

Passenger getPassenger()
 {
    try
    {
       Passenger flier = 
 	object.searchPassengerFlightRecord("Jane Doe");

    catch(NoPassengerFoundException npfe)
    {
       //какая-то работа
    }
 }

Случай

Passenger getPassenger()
 {
    Passenger flier = 
 	object.searchPassengerFlightRecord("Jane Doe");
       if(flier == null)
          //какая-то работа
 }

В случае 1, при неудачном поиске пассажира, вызывается NoPassengerFoundException; а в случае 2, простая проверка на null проверяет успешность. Разработчики часто встречаются с подобными ситуациями в повседневной работе; фокус в проектировании эффективной стратегии.

Итак, после общей философии об исключениях, что вы должны делать при возможности того, что поиск ничего не отыщет? Если результат поиска пуст, это не более чем случай нормальной работы? Поэтому, для логичного использования исключений, предпочтите подход в случае 2, а не в случае 1. (Да — мы учитываем влияние на производительность. Если бы этот код был бы в компактном цикле, тогда бы многократные вычисления if могли бы ухудшить производительность. Однако, действительное влияние выражения if на критичном участке на производительность можно узнать только после профайлинга и обширного анализа производительности. Эмпирические результаты показывают, что пытась писать максимально производительный код при игнорировании принципов дизайна чаще приносит больше вреда чем пользы. Итак, начните сначала и проектируйте вашу систему правильно, и затем измените позже при необходимости.)

Хороший пример исключительной ситуации: — Если экземпляр объекта, являющийся результатом вызова метода поиска был null, это приводит к фундаментальному нарушению семантики методов класса getPassenger. Для понимания влияния исключений на производительность прочтите параграф о производительности.

Как я могу наилучшим образом использовать исключения?

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

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

  1. Перехват и обработка всех исключений.
  2. Объявление исключений в секции throws метода и передача исключений вызывающему методу.
  3. Перехват исключений и преобразование его к другому классу и повторный вызов исключения.

Давайте рассмотрим эти случаи и попытаемcя разработать реальное решение.

Случай 1

Passenger getPassenger()
 {
    try
    {
       Passenger flier = 
 	object.searchPassengerFlightRecord("John Doe");
    }
    catch(MalformedURLException ume)
    {
       //какая-то обработка
    }
    catch(SQLException sqle)
    {
       //какая-то обработка
    }
 }

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

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

Случай 2

Passenger getPassenger() throws MalformedURLException, SQLException
 {
    Passenger flier = 
 	object.searchPassengerFlightRecord("John Doe");
 }

Случай 2 является другой крайностью. Метод getPassenger() объявляет все исключения, вызываемые данным методом в throws секции. Таким образом, getPassenger() знает о всех возможных исключительных ситуацияхt, но предпочитает не обрабатывать их а передать их вызывающему методу. То есть происходит передача исключения вызываемому методу. Однако это не является жизнеспособным решением, так как вся ответственность за обработку ошибок "всплывает наверх" — или перемещается вверх по иерархии — что может вызвать значительные проблемы, особенно при переносе с однойсплатформы на другую. Представьте, например, что вы Sabre (система бронирования авиабилетов), и метод searchPassengerFlightRecord() — часть вашего API для пользователя вашей системы, например Travelocity.com. Приложение Travelocity, которое включает в себя getPassenger(), как часть системы, должно работать с каждым исключением, генерируемым вашей системой. Также, приложение может не интересовать то, какое исключение вызвано: MalformedURLException или SQLException, его только заботит что-то вроде "Поиск неудачен". Рассмотрим это в примере 3.

Случай 3

Passenger getPassenger() throws TravelException
 {
    try
    {
       Passenger flier = 
 	object.searchPassengerFlightRecord("Gary Smith");
    }
    catch(MalformedURLException ume)
    {
       //какая-то обработка
          throw new TravelException(
 	"Search Failed", ume);
    }
    catch(SQLException sqle)
    {
       //какая-то обработка
       throw new TravelException(
 	"Search Failed", sqle);
    }
 }

Случай 3 находится посередине между двумя крайностями случаев 1 и 2, используя свой класс-исключение TravelException. Этот класс понимает действительно произошедшие исключения, передаваемые ему в качестве аргумента и преобразует их из сообщений системного уровня в сообщение прикладного уровня. Так вы сохраняете гибкость знания о действительно произошедшем исключении путем сохранения оригинального исключения как части экземпляра нового объекта-исключения, что упрощает отладку. Этот подход предоставляет элегантный метод проектирования API и кросс-платформенных приложений.

Следующий код демонстрирует класс TravelException, который мы использовали в качестве собственного исключения в примере 3. Он принимает два аргумента. Один из них — сообщение, которое может быть выведено в поток ошибок; другой — реальное исключение, которое привело к вызову нашего исключения. Этот код показывает как можно сохранить другую информациб внутри пользовательского исключения. Преимущество этого сохранения состоит в том, что если вызываемый метод захочет узнать реальнуб причину вызова TravelException, он всего лишь должен вызвать метод getHiddenException(). Это позволяет вызывающемому методу решить, нужно ли работать со специфичным исключением или достаточно обработки TravelException.

public class TravelException extends Exception
 {
    private Exception hiddenException_;
    public TravelException(String error, Exception excp)
    {
       super(error);
       hiddenException_ = excp;
    }
    public Exception getHiddenException()
    {
       return(hiddenException_);
    }
 }

Каково влияние на производительность?

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

Стек вызовов Java показывает вызванные методы. Вершиной является текущий выполняемый метод.
Рисунок 1. Стек вызовов Java показывает вызванные методы. Вершиной является текущий выполняемый метод.

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

Так, с целью нашего сравнения, что требуется в случае 1? Если NoPassengerFoundException вызывается при выполнении метода searchPassengerFlightRecord() (вершина стека), дальнейшее выполнение кода прерывается и JVM получает управление для осуществления механизма обработки ошибок Java. Затем виртуальная машина Java ищет в текущем методе секцию catch для исключения NoPassengerFoundException. Если секция не найдена, тогда JVM выталкивает кадр текущего метода из стека, и вызывающий метод становится текущим. JVM снова ищет в текущем методе, который ранее был вызывающим, подходящую catch секцию. Этот цикл повторяется до нахождения подходящего обработчика исключения. Или, если нить является последней нефоновой нитью, тогда приложение прерывается с распечаткой стека вызовов на консоли. В кратце, применение JVM для обработки вызванного исключения требует больше ресурсов, т.е. принудительное завершение метода является значительно более дорогим в плане производительности, чем нормальное завершение метода.

Заключение

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

Об авторах

Гурав Пал — консультант в RDA. Он имеет степень бакалавра компьютерных наук с более чем шестилетним опытом разработки программного обеспечения, втом числе болле трех лет на Java. Он сертифицирован Sun как Java 2 разработчик и Java 1.1 программист. Гурав часто публикуется в IEEE Potentials and Computer Communications UK и поддерживает собственный веб-сайт JavaPal.com.

Сонал Банзал в настоящее время преподает компьютерные науки в Loyola колледже, в Индии. Он имеет более чем пятилетний опыт в программировании, закончив работу для известных организаций подобно Индийскому Железнодорожному Институту Электротехники (IRIEEN) и Электрического Департамента, Southern Railway. Он стал набожным джавистом, пройдя через C и другие языки.

Ресурсы

Reprinted with permission from the August 2000 edition of JavaWorld magazine. Copyright © ITworld.com, Inc., an IDG Communications company. View the original article at: http://www.javaworld.com/ javaworld/jw-08-2000/jw-0818-exceptions.html

[an error occurred while processing this directive]
[an error occurred while processing this directive] Перевод на русский © Алексей Коштерек, 2000
< Вернуться на caйт :: Copyright © 1999 — 2010, IT • archiv.