Содержание
- 10: Обработка ошибок с помощью исключений
- Основные исключения
- Аргументы исключения
- Ловля исключения
- Блок try
- Обработчики исключений
- Прерывание против возобновления
- Создание ваших собственных исключений
- Спецификация исключения
- Перехват любого исключения
- Повторное выбрасывание исключений
- Стандартные исключения Java
- Особый случай RuntimeException
- Выполнение очистки с помощью finally
- Для чего нужно finally?
- Ловушка: потерянное исключение
- Ограничения исключений
- Конструкторы
- Совпадение исключений
- Руководство по исключениям
- Резюме
- Упражнения
10: Обработка ошибок с помощью исключений
Основная философия Java в том, что “плохо сформированный код не будет работать”.
Идеальное время для поимки ошибки — это время компиляции, прежде чем вы попробуете даже запустить программу. Однако не все ошибки могут быть определены во время компиляции. Оставшиеся проблемы должны быть обработаны во время выполнения, с помощью некоторого правила, которая позволяет источнику ошибки передавать соответствующую информацию приемщику, который будет знать, как правильно обрабатывать затруднение.
В C и других ранних языках могло быть несколько таких правил, и они обычно устанавливались соглашениями, а не являлись частью языка программирования. Обычно вы возвращали специальное значение или устанавливали флаг, а приемщику предлагалось взглянуть на это значение или на флаг и определить, было ли что-нибудь неправильно. Однако, по прошествии лет, было обнаружено, что программисты, использующие библиотеки, имеют тенденцию думать о себе, как о непогрешимых, например: “Да, ошибки могут случаться с другими, но не в моем коде”. Так что, не удивительно, что они не проверяют состояние ошибки (а иногда состояние ошибки бывает слишком глупым, чтобы проверять [51] ). Если вы всякий раз проверяли состояние ошибки при вызове метода, ваш код мог превратиться нечитаемый ночной кошмар. Поскольку программисты все еще могли уговорить систему в этих языках, они были стойки к принятию правды: Этот подход обработки ошибок имел большие ограничения при создании больших, устойчивых, легких в уходе программ.
Решением является упор на причинную натуру обработки ошибок и усиление правил. Это действительно имеет долгую историю, так как реализация обработки исключений возвращает нас к операционным системам 1960-х и даже к бейсиковому “ on error goto ” (переход по ошибке). Но исключения C++ основывались на Ada, а Java напрямую базируется на C++ (хотя он больше похож на Object Pascal).
Слово “исключение” используется в смысле “Я беру исключение из этого”. В том месте, где возникает проблема, вы можете не знать, что делать с ней, но вы знаете, что вы не можете просто весело продолжать; вы должны остановиться и кто-то, где-то должен определить, что делать. Но у вас нет достаточно информации в текущем контексте для устранения проблемы. Так что вы передаете проблему в более высокий контекст, где кто-то будет достаточно квалифицированным, чтобы принять правильное решение (как в цепочке команд).
Другая, более значимая выгода исключений в том, что они очищают код обработки ошибок. Вместо проверки всех возможных ошибок и выполнения этого в различных местах вашей программы, вам более нет необходимости проверять место вызова метода (так как исключение гарантирует, что кто-то поймает его). И вам необходимо обработать проблему только в одном месте, называемом обработчик исключения. Это сохранит ваш код и разделит код, описывающий то, что вы хотите сделать, от кода, который выполняется, если что-то случается не так. В общем, чтение, запись и отладка кода становится яснее при использовании исключений, чем при использовании старого способа обработки ошибок.
Так как обработка исключений навязывается компилятором Java, то есть так много примеров, которые могут быть написаны в этой книге без изучения обработки исключений. Эта глава вводит вас в код, который вам необходим для правильной обработки исключений, и способы, которыми вы можете генерировать свои собственные исключения, если ваш метод испытывает затруднения.
Основные исключения
Исключительное состояние — это проблема, которая мешает последовательное исполнение метода или ограниченного участка, в котором вы находитесь. Важно различать исключительные состояния и обычные проблемы, в которых вы имеете достаточно информации в текущем контексте, чтобы как-то справиться с трудностью. В исключительном состоянии вы не можете продолжать обработку, потому что вы не имете необходимой информации, чтобы разобраться с проблемой в текущем контексте. Все, что вы можете сделать — это выйти из текущего контекста и отослать эту проблему к высшему контексту. Это то, что случается, когда вы выбрасываете исключение.
Простой пример — деление. Если вы делите на ноль, стоит проверить, чтобы убедиться, что вы пройдете вперед и выполните деление. Но что это значит, что делитель равен нулю? Может быть, вы знаете, в контексте проблемы вы пробуете решить это в определенном методе, как поступать с делителем, равным нулю. Но если это не ожидаемое значение, вы не можете это определить внутри и раньше должны выбросить исключение, чем продолжать свой путь.
Когда вы выбрасываете исключение, случается несколько вещей. Во-первых, создается объект исключения тем же способом, что и любой Java объект: в куче, с помощью new . Затем текущий путь выполнения (который вы не можете продолжать) останавливается, и ссылка на объект исключения выталкивается из текущего контекста. В этот момент вступает механизм обработки исключений и начинает искать подходящее место для продолжения выполнения программы. Это подходящее место — обработчик исключения, чья работа — извлечь проблему, чтобы программа могла попробовать другой способ, либо просто продолжиться.
Простым примером выбрасывания исключения является рассмотрение ссылки на объект, называемой t . Возможно, что вы можете передать ссылку, которая не была инициализирована, так что вы можете пожелать проверить ее перед вызовом метода, использующего эту ссылку на объект. Вы можете послать информацию об ошибке в больший контекст с помощью создания объекта, представляющего вашу информацию и “выбросить” его из вашего контекста. Это называется выбрасыванием исключения. Это выглядит так:
Здесь выбрасывается исключение, которое позволяет вам — в текущем контексте — отказаться от ответственности, думая о будущем решении. Оно магически обработается где-то в другом месте. Где именно будет скоро показано.
Аргументы исключения
Как и многие объекты в Java, вы всегда создаете исключения в куче, используя new , который резервирует хранилище и вызывает конструктор. Есть два конструктора для всех стандартных исключений: первый — конструктор по умолчанию, и второй принимает строковый аргумент, так что вы можете поместить подходящую информацию в исключение:
Эта строка позже может быть разложена при использовании различных методов, как скоро будет показано.
Ключевое слово throw является причиной несколько относительно магических вещей. Обычно, вы сначала используете new для создания объекта, который соответствует ошибочному состоянию. Вы передаете результирующую ссылку в throw . Объект, в результате, “возвращается” из метода, даже если метод обычно не возвращает этот тип объекта. Простой способ представлять себе обработку исключений, как альтернативный механизм возврата, хотя вы будете иметь трудности, если будете использовать эту аналогию и далее. Вы можете также выйти из обычного блока, выбросив исключение. Но значение будет возвращено, и произойдет выход из метода или блока.
Любое подобие обычному возврату из метода здесь заканчивается, потому что куда вы возвращаетесь, полностью отличается от того места, куда вы вернетесь при нормальном вызове метода. (Вы закончите в соответствующем обработчике исключения, который может быть очень далеко — на много уровней ниже по стеку вызова — от того места, где выброшено исключение.)
В дополнение, вы можете выбросить любой тип Выбрасываемого(Throwable) объекта, который вы хотите. Обычно вы будете выбрасывать различные классы исключений для каждого различного типа ошибок. Информация об ошибке представлена и внутри объекта исключения, и выбранным типом исключения, так что кто-то в большем контексте может определить, что делать с вашим исключением. (Часто используется только информация о типе объекта исключения и ничего значащего не хранится в объекте исключения.)
Ловля исключения
Если метод выбросил исключение, он должен предполагать, что исключение будет “поймано” и устранено. Один из преимуществ обработки исключений Java в том, что это позволяет вам концентрироваться на проблеме, которую вы пробуете решить в одном месте, а затем принимать меры по ошибкам из этого кода в другом месте.
Чтобы увидеть, как ловятся исключения, вы должны сначала понять концепцию критического блока . Он является секцией кода, которая может произвести исключение и за которым следует код, обрабатывающий это исключение.
Блок try
Если вы находитесь внутри метода, и вы выбросили исключение (или другой метод, вызванный вами внутри этого метода, выбросил исключение), такой метод перейдет в процесс бросания. Если вы не хотите быть выброшенными из метода, вы можете установить специальный блок внутри такого метода для поимки исключения. Он называется блок проверки, потому что вы “проверяете” ваши различные методы, вызываемые здесь. Блок проверки — это обычный блок, которому предшествует ключевое слово try :
Если вы внимательно проверяли ошибки в языке программирования, который не поддерживает исключений, вы окружали каждый вызов метода кодом установки и проверки ошибки, даже если вы вызывали один и тот же метод несколько раз. С обработкой исключений вы помещаете все в блок проверки и ловите все исключения в одном месте. Это означает, что ваш код становится намного легче для написания и легче для чтения, поскольку цель кода — не смешиваться с проверкой ошибок.
Обработчики исключений
Конечно, выбрасывание исключения должно где-то заканчиваться. Это “место” — обработчик исключения , и есть один обработчик для каждого типа исключения, которые вы хотите поймать. Обработчики исключений следуют сразу за блоком проверки и объявляются ключевым словом catch :
Каждое catch предложение (обработчик исключения) как меленький метод, который принимает один и только один аргумент определенного типа. Идентификаторы ( id1 , id2 и так далее) могут быть использованы внутри обработчика, как аргумент метода. Иногда вы нигде не используете идентификатор, потому что тип исключения дает вам достаточно информации, чтобы разобраться с исключением, но идентификатор все равно должен быть.
Обработчики должны располагаться прямо после блока проверки. Если выброшено исключение, механизм обработки исключений идет охотится за первым обработчиком с таким аргументом, тип которого совпадает с типом исключения. Затем происходит вход в предложение catch, и рассматривается обработка исключения. Поиск обработчика, после остановки на предложении catch, заканчивается. Выполняется только совпавшее предложение catch; это не как инструкция switch , в которой вам необходим break после каждого case , чтобы предотвратить выполнение оставшейся части.
Обратите внимание, что внутри блока проверки несколько вызовов различных методов может генерировать одно и тоже исключение, но вам необходим только один обработчик.
Прерывание против возобновления
Есть две основные модели в теории обработки исключений. При прерывании (которое поддерживает Java и C++), вы предполагаете, что ошибка критична и нет способа вернуться туда, где возникло исключение. Кто бы ни выбросил исключение, он решил, что нет способа спасти ситуацию, и он не хочет возвращаться обратно.
Альтернатива называется возобновлением — это означает, что обработчик исключения может что-то сделать для исправления ситуации, а затем повторно вызовет придирчивый метод, предполагая, что вторая попытка будет удачной. Если вы хотите возобновления, это означает, что вы все еще надеетесь продолжить выполнение после обработки исключения. В этом случае ваше исключение больше похоже на вызов метода, в котором вы должны произвести настройку ситуации в Java, после чего возможно возобновление. (То есть, не выбрасывать исключение; вызвать метод, который исправит проблему.) Альтернатива — поместить ваш блок try внутри цикла while , который производит повторный вход в блок try , пока не будет получен удовлетворительный результат.
Исторически программисты используют операционные системы, которые поддерживают обработку ошибок с возобновлением, в конечном счете, заканчивающуюся использованием прерывающего кода и пропуском возобновления. Так что, хотя возобновление на первый взгляд кажется привлекательнее, оно не так полезно на практике. Вероятно, главная причина — это с оединение таких результатов: ваш обработчик часто должен знать, где брошено исключение и содержать не характерный специфический код для места выброса. Это делает код трудным для написания и ухода, особенно для больших систем, где исключения могут быть сгенерированы во многих местах.
Создание ваших собственных исключений
Вы не ограничены в использовании существующих Java исключений. Это очень важно, потому что часто вам будет нужно создавать свои собственные исключения, чтобы объявить специальную ошибку, которую способна создавать ваша библиотека, но это не могли предвидеть, когда создавалась иерархия исключений Java.
Для создания вашего собственного класса исключения вы обязаны наследовать его от исключения существующего типа, предпочтительно от того, которое наиболее близко подходит для вашего нового исключения (однако, часто это невозможно). Наиболее простой способ создать новый тип исключения — это просто создать конструктор по умолчанию для вас, так чтобы он совсем не требовал кода:
Когда компилятор создает конструктор по умолчанию, он автоматически (и невидимо) вызывает конструктор по умолчанию базового класса. Конечно, в этом случае у вас нет конструктора SimpleException(String) , но на практике он не используется часто. Как вы увидите, наиболее важная вещь в использовании исключений — это имя класса, так что чаще всего подходят такие исключения, как показаны выше.
Вот результат, который печатается на консоль стандартной ошибки — поток для записи в System.err . Чаще всего это лучшее место для направления информации об ошибках, чем System.out , который может быть перенаправлен. Если вы посылаете вывод в System.err , он не может быть перенаправлен, в отличие от System.out , так что пользователю легче заметить его.
Создание класса исключения, который также имеет конструктор, принимающий String , также достаточно просто:
Дополнительный код достаточно мал — добавлено два конструктора, которые определяют способы создания MyException . Во втором конструкторе явно вызывается конструктор базового класса с аргументом String с помощью использования ключевого слова super .
Информация трассировки направляется в System.err , так как это лучше, поскольку она будет выводиться, даже если System.out будет перенаправлен.
Программа выводит следующее:
Вы можете увидеть недостаток деталей в этих сообщениях MyException , выбрасываемых из f( ) .
Процесс создания вашего собственного исключения может быть развит больше. Вы можете добавить дополнительные конструкторы и члены:
Бал добавлен член — данные i , вместе с методами, которые читают его значение и дополнительные конструкторы, которые устанавливают его. Вод результат работы:
Так как исключение является просто еще одним видом объекта, вы можете продолжать этот процесс наращивания мощность ваших классов исключений. Однако запомните, что все это украшение может быть потеряно для клиентского программиста, использующего ваш пакет, так как он может просто взглянуть на выбрасываемое исключение и ничего более. (Это способ чаще всего используется в библиотеке исключений Java.)
Спецификация исключения
В Java, вам необходимо проинформировать клиентских программистов, которые вызывают ваши методы, что метод может выбросить исключение. Это достаточно цивилизованный метод, поскольку тот, кто производит вызов, может точно знать какой код писать для поимки всех потенциальных исключений. Конечно, если доступен исходный код, клиентский программист может открыть программу и посмотреть на инструкцию throw , но часто библиотеки не поставляются с исходными текстами. Для предотвращения возникновения этой проблемы Java обеспечивает синтаксис (и навязывает вам этот синтаксис), позволяющий вам правильно сказать клиентскому программисту, какое исключение выбрасывает этот метод, так что клиентский программист может обработать его. Это спецификация исключения и это часть объявления метода, добавляемая после списка аргументов.
Спецификация исключения использует дополнительное ключевое слово throws , за которым следует за список потенциальных типов исключений. Так что определение вашего метода может выглядеть так:
Если вы скажете
это будет означать, что исключения не выбрасываются из этого метода. (Кроме исключения, типа RuntimeException , которое может быть выброшено в любом месте — это будет описано позже.)
Вы не можете обмануть спецификацию исключения — если ваш метод является причиной исключения и не обрабатывает его, компилятор обнаружит это и скажет вам что вы должны либо обработать исключение, либо указать с помощью спецификации исключения, что оно может быть выброшено из вашего метода. При введении ограничений на спецификацию исключений с верху вниз, Java гарантирует, что исключение будет корректно обнаружено во время компиляции[52].
Есть одно место, в котором вы можете обмануть: вы можете заявить о выбрасывании исключения, которого на самом деле нет. Компилятор получит ваши слова об этом и заставит пользователя вашего метода думать, что это исключение на самом деле выбрасывается. Это имеет благотворный эффект на обработчика этого исключения, так как вы на самом деле позже можете начать выбрасывать это исключение и это не потребует изменения существующего кода. Также важно создание абстрактного базового класса и интерфейсов , наследующих классам или реализующим многие требования по выбрасыванию исключений.
Перехват любого исключения
Можно создать обработчик, ловящий любой тип исключения. Вы сделаете это, перехватив исключение базового типа Exception (есть другие типы базовых исключений, но Exception — это базовый тип, которому принадлежит фактически вся программная активность):
Это поймает любое исключение, так что, если вы используете его, вы будете помещать его в конце вашего списка обработчиков для предотвращения перехвата любого обработчика исключения, который мог управлять течением.
Так как класс Exception — это базовый класс для всех исключений, которые важны для программиста, вы не получите достаточно специфической информации об исключении, но вы можете вызвать метод, который пришел из его базового типа Throwable :
String getMessage( )
String getLocalizedMessage ( )
Получает подробное сообщение или сообщение, отрегулированное по его месту действия.
String toString( )
Возвращает короткое описание Throwable, включая подробности сообщения, если они есть.
void printStackTrace( )
void printStackTrace(PrintStream)
void printStackTrace ( PrintWriter )
Печатает Throwable и трассировку вызовов Throwable. Вызов стека показывает последовательность вызовов методов, которые подвели вас к точке, в которой было выброшено исключение. Первая версия печатает в поток стандартный поток ошибки, второй и третий печатают в выбранный вами поток (в Главе 11, вы поймете, почему есть два типа потоков).
Throwable fillInStackTrace ( )
Запись информации в этот Throwable объекте о текущем состоянии кадра стека. Это полезно, когда приложение вновь выбрасывает ошибки или исключение (дальше об этом будет подробнее).
Кроме этого вы имеете некоторые другие метода, наследуемые от базового типа Throwable Object (базовый тип для всего). Один из них, который может быть удобен для исключений, это getClass( ) , который возвращает объектное представление класса этого объекта. Вы можете опросить у объекта этого Класса его имя с помощью getName( ) или toString( ) . Вы также можете делать более изощренные вещи с объектом Класса, которые не нужны в обработке ошибок. Объект Class будет изучен позже в этой книге.
Вот пример, показывающий использование основных методов Exception :
Вывод этой программы:
Вы можете заметить, что методы обеспечивают больше информации — каждый из них дополняет предыдущий.
Повторное выбрасывание исключений
Иногда вам будет нужно вновь выбросить исключение, которое вы только что поймали, обычно это происходит, когда вы используете Exception , чтобы поймать любое исключение. Так как вы уже имеете ссылку на текущее исключение, вы можете просто вновь бросить эту ссылку:
Повторное выбрасывание исключения является причиной того, что исключение переходит в обработчик следующего, более старшего контекста. Все остальные предложения catch для того же самого блока try игнорируются. Кроме того, все, что касается объекта исключения, сохраняется, так что обработчик старшего контекста, который поймает исключение этого специфического типа, может получить всю информацию из этого объекта.
Если вы просто заново выбросите текущее исключение, то информация, которую вы печатаете об этом исключении, в printStackTrace( ) будет принадлежать источнику исключения, а не тому месту, откуда вы его вновь выбросили. Если вы хотите установить новый стек информации трассировки, вы можете сделать это, вызвав функцию fillInStackTrace( ) , которая возвращает объект исключения, для которого текущий стек наполняется информацией для старого объекта исключения. Вот как это выглядит:
Важные строки помечены комментарием с числами. При раскомментированной строке 17 (как показано), на выходе получаем:
Так что стек трассировки исключения всегда помнит исходное место, не имеет значения, сколько прошло времени перед повторным выбрасыванием.
Если закомментировать строку 17, а строку 18 раскомментировать, будет использоваться функция fillInStackTrace( ) , и получим результат:
Поскольку fillInStackTrace( ) в строке 18 становится новой исходной точкой исключения.
Класс Throwable должен появиться в спецификации исключения для g( ) и main( ) , потому что fillInStackTrace( ) производит ссылку на объект Throwable . Так как Throwable — это базовый класс для Exception , можно получить объект, который является Throwable , но не Exception , так что обработчик для Exception в main( ) может промахнуться. Чтобы убедится, что все в порядке, компилятор навязывает спецификацию исключения для Throwable . Например, исключение в следующем примере не перехватывается в main( ) :
Также возможно вновь выбросить исключение, отличающееся от того, которое вы поймали. Если вы делаете это, вы получаете сходный эффект, как если бы вы использовали fillInStackTrace( ) — информация об оригинальном состоянии исключения теряется, а то, с чем вы остаетесь — это информация, относящаяся к новому throw :
Вот что напечатается:
Конечное исключение знает только то, что оно произошло в main( ) , а не в f( ) .
Вам никогда не нужно заботится об очистке предыдущего исключения или что другое исключение будет иметь значение. Они являются объектами, базирующимися в куче и создающимися с помощью new , так что сборщик мусора автоматически очистит их все.
Стандартные исключения Java
Класс Java Throwable описывает все, что может быть выброшено как исключение. Есть два основных типа объектов Throwable (“тип” = “наследуется от”). Error представляет ошибки времени компиляции и системные ошибки, о поимке которых вам не нужно беспокоиться (за исключением особых случаев). Exception — основной тип, который может быть выброшен из любого стандартного метода библиотеки классов Java и из вашего метода, что случается во время работы. Так что основной тип, интересующий программистов Java — это Exception .
Лучший способ получить обзор исключений — просмотреть HTML документацию Java, которую можно загрузить с java.sun.com. Это стоит сделать один раз, чтобы почувствовать разнообразие исключений, но вы скоро увидите, что нет никакого специального отличия одного исключения от другого кроме его имени. Кроме того, число исключений в Java увеличивается, поэтому бессмысленно перечислять их в книге. Каждая новая библиотека, получаемая от третьих производителей, вероятно, имеет свои собственные исключения. Важно понимать концепцию и то, что вы должны делать с исключением.
Основная идея в том, что имя исключения представляет возникшую проблему, и имя исключения предназначено для самообъяснения. Не все исключения определены в java.lang , некоторые создаются для поддержки других библиотек, таких как util , net и io , как вы можете видеть по полому имени класса или по их наследованию. Например, все исключения I/O наследуются от java.io.IOException .
Особый случай RuntimeException
Первый пример в этой главе был:
Это может быть немного пугающим: думать, что вы должны проверять на null каждую ссылку, передаваемую в метод (так как вы не можете знать, что при вызове была передана правильная ссылка). К счастью вам не нужно это, поскольку Java выполняет стандартную проверку во время выполнения за вас и, если вы вызываете метод для null ссылки, Java автоматически выбросит NullPointerException . Так что приведенную выше часть кода всегда излишняя.
Есть целая группа типов исключений, которые относятся к такой категории. Они всегда выбрасываются Java автоматически и вам не нужно включать их в вашу спецификацию исключений. Что достаточно удобно, что они все сгруппированы вместе и относятся к одному базовому классу, называемому RuntimeException , который является великолепным примером наследования: он основывает род типов, которые имеют одинаковые характеристики и одинаковы в поведении. Также вам никогда не нужно писать спецификацию исключения, объявляя, что метод может выбросить RuntimeException , так как это просто предполагается. Так как они указывают на ошибки, вы, фактически, никогда не выбрасываете RuntimeException — это делается автоматически. Если вы заставляете ваш код выполнять проверку на RuntimeException s, он может стать грязным. Хотя вы обычно не ловите RuntimeExceptions , в ваших собственных пакетах вы можете по выбору выбрасывать некоторые из RuntimeException .
Что случится, если вы не выбросите это исключение? Так как компилятор не заставляет включать спецификацию исключений для этого случая, достаточно правдоподобно, что RuntimeException могут принизывать насквозь ваш метод main( ) и не ловится. Чтобы увидеть, что случится в этом случае, попробуйте следующий пример:
Вы уже видели, что RuntimeException (или любое, унаследованное от него) — это особый случай, так как компилятор не требует спецификации этих типов.
Вот что получится при выводе:
Так что получим такой ответ: Если получаем RuntimeException , все пути ведут к выходу из main( ) без поимки, для такого исключения вызывается printStackTrace( ) , и происходит выход из программы.
Не упускайте из виду, что вы можете только игнорировать RuntimeException в вашем коде, так как вся другая обработка внимательно ограничивается компилятором. Причина в том, что RuntimeException представляют ошибки программы:
- Ошибка, которую вы не можете поймать (получение null ссылки, передаваемой в ваш метод клиентским программистом, например).
- Ошибки, которые вы, как программист, должны проверять в вашем коде (такие как ArrayIndexOutOfBoundsException , где вы должны обращать внимание на размер массива).
Вы можете увидеть какая огромная выгода от этих исключений, так как они помогают процессу отладки.
Интересно заметить, что вы не можете классифицировать обработку исключений Java, как инструмент с одним предназначением. Да, он предназначен для обработки этих надоедливых ошибок времени выполнения, которые будут случаться, потому что ограничения накладываются вне кода управления, но он также важен для определенных типов ошибок программирования, которые компилятор не может отследить.
Выполнение очистки с помощью finally
Часто есть такие места кода, которые вы хотите выполнить независимо от того, было ли выброшено исключение в блоке try , или нет. Это обычно относится к некоторым операциям, отличным от утилизации памяти (так как об этом заботится сборщик мусора). Для достижения этого эффекта вы используете предложение finally [53] в конце списка всех обработчиков исключений. Полная картина секции обработки исключений выглядит так:
Для демонстрации, что предложение finally всегда отрабатывает, попробуйте эту программу:
Эта программа также дает подсказку, как вы можете поступить с фактом, что исключения в Java (как и исключения в C++) не позволяют вам возвратится обратно в то место, откуда оно выброшено, как обсуждалось ранее. Если вы поместите ваш блок try в цикл, вы сможете создать состояние, которое должно будет встретиться, прежде чем вы продолжите программу. Вы также можете добавить статический счетчик или какое-то другое устройство, позволяющее циклу опробовать различные подходы, прежде чем сдаться. Этим способом вы можете построить лучший уровень живучести вашей программы.
Вот что получается на выводе:
Независимо от того, было выброшено исключение или не, предложение finally выполняется всегда.
Для чего нужно finally?
В языках без сборщика мусора и без автоматического вызова деструктора [54] , finally очень важно, потому что оно позволяет программисту гарантировать освобождение памяти независимо от того, что случилось в блоке try . Но Java имеет сборщик мусора, так что освобождение памяти, фактически, не является проблемой. Также, язык не имеет деструкторов для вызова. Так что, когда вам нужно использовать finally в Java?
finally необходимо, когда вам нужно что-то установить, отличное от блока памяти, в его оригинальное состояние. Это очистка определенного вида, такое как открытие файла или сетевого соединения, рисование на экране или даже переключение во внешний мир, как смоделировано в следующем примере:
Цель этого примера — убедится, что переключатель выключен, когда main( ) будет завершена, так что sw.off( ) помешена в конце блока проверки и в каждом обработчике исключения. Но возможно, что будет выброшено исключение, которое не будет поймано здесь, так что sw.off( ) будет пропущено. Однако с помощью finally вы можете поместить очищающий код для блока проверки только в одном месте:
Здесь sw.off( ) была перемещена только в одно место, где она гарантировано отработает не зависимо от того, что случится.
Даже в случае исключения, не пойманного в этом случае набором предложений catch , finally будет выполнено прежде, чем механизм обработки исключений продолжит поиск обработчика на более высоком уровне:
Вывод этой программы показывает что происходит:
Инструкция finally также будет исполнена в ситуации, когда используются инструкции break и continue . Обратите внимание, что наряду с помеченным break и помеченным continue , finally подавляет необходимость в использовании инструкции goto в Java.
Ловушка: потерянное исключение
Вообще, реализация исключений Java достаточно выдающееся, но, к сожалению, есть недостаток. Хотя исключения являются индикаторами кризиса в вашей программе и не должны игнорироваться, возможна ситуация, при которой исключение просто потеряется. Это случается при определенной конфигурации использования предложения finally :
Вот что получаем на выходе:
Вы можете видеть, что нет свидетельств о VeryImportantException , которое просто заменилось HoHumException в предложении finally . Это достаточно серьезная ловушка, так как это означает, что исключения могут быть просто потеряны и далее в более узких и трудно определимых ситуациях, чем показано выше. В отличие от Java, C++ трактует ситуации, в которых второе исключение выбрасывается раньше, чем обработано первое, как ошибку программирования. Надеюсь, что будущие версии Java решат эту проблему (с другой стороны, вы всегда окружаете метод, который выбрасывает исключение, такой как dispose( ) , предложением try-catch ).
Ограничения исключений
Когда вы перегружаете метод, вы можете выбросить только те исключения, которые указаны в версии базового класса этого метода. Это полезное ограничение, так как это означает, что код, работающий с базовым классом, будет автоматически работать с любым другим объектом, наследованным от базового класса (конечно, это фундаментальная концепция ООП), включая исключения.
Этот пример демонстрирует виды налагаемых ограничений (времени компиляции) на исключения:
В Inning вы можете увидеть, что и конструктор, и метод event( ) говорят о том, что они будут выбрасывать исключение, но они не делают этого. Это допустимо, потому что это позволяет вам заставить пользователя ловить любое исключение, которое может быть добавлено и перегруженной версии метода event( ) . Эта же идея применена к абстрактным методам, как видно в atBat( ) .
Интересен interface Storm , потому что он содержит один метод ( event( ) ), который определен в Inning , и один метод, которого там нет. Оба метода выбрасывают новый тип исключения: RainedOut . Когда StormyInning расширяет Inning и реализует Storm , вы увидите, что метод event( ) в Storm не может изменить исключение интерфейса event( ) в Inning . Кроме того, в этом есть здравый смысл, потому что, в противном случае, вы никогда не узнаете, что поймали правильную вещь, работая с базовым классом. Конечно, если метод, описанный как интерфейс, не существует в базовом классе, такой как rainHard( ) , то нет проблем, если он выбросит исключения.
Ограничения для исключений не распространяются на конструкторы. Вы можете видеть в StormyInning , что конструктор может выбросить все, что хочет, не зависимо от того, что выбрасывает конструктор базового класса. Но, так как конструктор базового класса всегда, так или иначе, должен вызываться (здесь автоматически вызывается конструктор по умолчанию), конструктор наследованного класса должен объявить все исключения конструктора базового класса в своей спецификации исключений. Заметьте, что конструктор наследованного класса не может ловить исключения, выброшенные конструктором базового класса.
Причина того, что StormyInning.walk( ) не будет компилироваться в том, что она выбрасывает исключение, которое Inning.walk( ) не выбрасывает. Если бы это допускалось, то вы могли написать код, вызывающий Inning.walk( ) , и не иметь обработчика для любого исключения, а затем, когда вы заменили объектом класса, унаследованного от Inning , могло начать выбрасываться исключение и ваш код сломался бы. При ограничивании методов наследуемого класса в соответствии со спецификацией исключений методов базового класса замена объектов допустима.
Перегрузка метода event( ) показывает, что версия метода наследованного класса может не выбрасывать исключение, даже если версия базового класса делает это. Опять таки это хорошо, так как это не нарушит ни какой код, который написан с учетом версии базового класса с выбрасыванием исключения. Сходная логика применима и к atBat( ) , которая выбрасывает PopFoul — исключение, унаследованное от Foul , выбрасываемое версией базового класса в методе atBat( ) . Таким образом, если кто-то напишет код, который работает с классом Inning и вызывает atBat( ) , он должен ловить исключение Foul . Так как PopFoul наследуется от Foul , обработчик исключения также поймает PopFoul .
Последнее, что нас интересует — это main( ) . Здесь вы можете видеть, что если вы имеете дело с объектом StormyInning , компилятор заставит вас ловить только те исключения, которые объявлены для этого класса, но если вы выполните приведение к базовому типу, то компилятор (что совершенно верно) заставит вас ловить исключения базового типа. Все эти ограничения производят более устойчивый код обработки исключений [55] .
Полезно понимать, что хотя спецификация исключений навязываются компилятором во время наследования, спецификация исключений не является частью метода типа, который включает только имя метода и типы аргументов. Поэтому вы не можете перегрузить метод, основываясь на спецификации исключений. Кроме того, только потому, что спецификация исключений существует в версии метода базового класса, это не означает, что она должна существовать в версии метода наследованного класса. Это немного отличается от правил наследования, по которым метод базового класса должен также существовать в наследуемом классе. Есть другая возможность: “спецификации исключения интерфейса” для определенного метода может сузиться во время наследования и перегрузки, но он не может расшириться — это точно противоречит правилам для интерфейса класса при наследовании.
Конструкторы
Когда пишете код с исключениями, обычно важно, чтобы вы всегда спрашивали: “Если случится исключение, будет ли оно правильно очищено?” Большую часть времени вы этим сохраните, но в конструкторе есть проблемы. Конструктор переводит объект в безопасное начальное состояние, но он может выполнить некоторые операции — такие как открытие файла — которые не будут очищены, пока пользователь не закончит работать с объектом и не вызовет специальный очищающий метод. Если вы выбросили исключение из конструктора, это очищающее поведение может не сработать правильно. Это означает, что вы должны быть особенно осторожными при написании конструктора.
Так как вы только изучили о finally , вы можете подумать, что это корректное решение. Но это не так просто, потому что finally выполняет очищающий код каждый раз, даже в ситуации, в которой вы не хотите, чтобы выполнялся очищающий код до тех пор, пока не будет вызван очищающий метод. Таким образом, если вы выполняете очистку в finally , вы должны установить некоторый флаг, когда конструктор завершается нормально, так что вам не нужно ничего делать в блоке finally , если флаг установлен. Потому что это обычно не элегантное решение (вы соединяете ваш код в одном месте с кодом в другом месте), так что лучше попробовать предотвратить выполнение такого рода очистки в finally , если вы не вынуждены это делать.
В приведенном ниже примере класс, называемый InputFile , при создании открывает файл и позволяет вам читать его по одной строке (конвертируя в String ). Он использует классы FileReader и BufferedReader из стандартной библиотеки Java I/O, которая будет обсуждаться в Главе 11, но которая достаточно проста, что вы, вероятно, не будете иметь трудностей в понимании основ ее использования:
Конструктор для InputFile получает аргумент String , который является именем файла, который вы открываете. Внутри блока try создается FileReader с использование имени файла. FileReader не очень полезен до тех пор, пока вы не используете его для создания BufferedReader , с которым вы фактически можете общаться — обратите внимание, что в этом одна из выгод InputFile , который комбинирует эти два действия.
Если конструктор FileReader завершится неудачно, он выбросит FileNotFoundException , которое должно быть поймано отдельно, потому что это тот случай, когда вам не надо закрывать файл, так как его открытие закончилось неудачно. Любое другое предложение catch должно закрыть файл, потому что он был открыт до того, как произошел вход в предложение catch. (Конечно это ненадежно, если более одного метода могут выбросить FileNotFoundException . В этом случае вы можете захотеть разбить это на несколько блоков try .) Метод close( ) может выбросить исключение, так что он проверяется и ловится, хотя он в блоке другого предложения catch — это просто другая пара фигурных скобок для компилятора Java. После выполнения локальных операций исключение выбрасывается дальше, потому что конструктор завершился неудачей, и вы не захотите объявить, что объект правильно создан и имеет силу.
В этом примере, который не использует вышеупомянутую технику флагов, предложение finally определенно это не то место для закрытия файла, так как он будет закрываться всякий раз по завершению конструктора. Так как вы хотим, чтобы файл был открыт для использования все время жизни объекта InputFile , этот метод не подходит.
Метод getLine( ) возвращает String , содержащую следующую строку файла. Он вызывает readLine( ) , который может выбросить исключение, но это исключение ловится, так что getLine( ) не выбрасывает никаких исключений. Одна из проблем разработки исключений заключается в том, обрабатывать ли исключение полностью на этом уровне, обрабатывать ли его частично и передавать то же исключение (или какое-то другое) или просто передавать его дальше. Дальнейшая передача его, в подходящих случаях, может сильно упростить код. Метод getLine( ) превратится в:
Но, конечно, теперь вызывающий код несет ответственность за обработку любого исключения IOException , которое может возникнуть .
Метод cleanup( ) должен быть вызван пользователем, когда закончится использование объекта InputFile . Это освободит ресурсы системы (такие как указатель файла), которые используются объектами BufferedReader и/или FileReader [56] . Вам не нужно делать этого до тех пор, пока вы не закончите работать с объектом InputFile . Вы можете подумать о перенесении такой функциональности в метод finalize( ) , но как показано в Главе 4, вы не можете всегда быть уверены, что будет вызвана finalize( ) (даже если вы можете быть уверены, что она будет вызвана, вы не будете знать когда). Это обратная сторона Java: вся очистка — отличающаяся от очистки памяти — не происходит автоматически, так что вы должны информировать клиентского программиста, что он отвечает за это и, возможно, гарантировать возникновение такой очистки с помощью finalize( ) .
В Cleanup.java InputFile создается для открытия того же исходного файла, который создает программа, файл читается по строкам, а строки нумеруются. Все исключения ловятся в основном в main( ) , хотя вы можете выбрать лучшее решение.
Польза от этого примера в том, что он показывает вам, почему исключения введены именно в этом месте книги — вы не можете работать с основами ввода/вывода, не используя исключения. Исключения настолько интегрированы в программирование на Java, особенно потому, что компилятор навязывает их, что вы можете выполнить ровно столько, не зная их, сколько может сделать, работая с ними.
Совпадение исключений
Когда выброшено исключение, система обработки исключений просматривает “ближайшие” обработчики в порядке их записи. Когда он находит совпадение, исключение считается обработанным и дальнейшего поиска не производится.
Для совпадения исключения не требуется точного соответствия между исключением и его обработчиком. Объект наследованного класса будет совпадать обработчику базового класса, как показано в этом примере:
Исключение Sneeze будет поймано первым предложением catch , с которым оно совпадает — конечно, это первое предложение. Конечно, если вы удалите первое предложение catch, оставив только:
Код все равно будет работать, потому что он ловит базовый класс Sneeze . Другими словами, catch(Annoyance e) будет ловить Annoyance или любой другой класс, наследованный от него. Это полезно, потому что, если вы решите добавить еще унаследованных исключений в метод, то код клиентского программиста не будет требовать изменений до тех пор, пока клиент ловит исключения базового класса.
Если вы пробуете “маскировать” исключения наследованного класса, помещая первым предложение catch для базового класса, как здесь:
компилятор выдаст вам сообщение об ошибке, так как catch-предложение Sneeze никогда не будет достигнуто.
Руководство по исключениям
Используйте исключения для:
- Исправления проблем и нового вызова метода, который явился причиной исключения.
- Исправления вещей и продолжения без повторной попытки метода.
- Подсчета какого-то альтернативного результата вместо того, который должен был вычислить метод.
- Выполнения того, что вы можете в текущем контексте и повторного выброса того же исключения в более старший контекст.
- Выполнения того, что вы можете в текущем контексте и повторного выброса другого исключения в более старший контекст.
- Прекращения программы .
- Упрощения. (Если ваша схема исключений делает вещи более сложными, то это приводит к тягостному и мучительному использованию.)
- Создать более безопасные библиотеки и программы. (Для краткосрочной инвестиции — для отладки — и для долгосрочной инвестиции (Для устойчивости приложения).)
Резюме
Улучшение перекрытия ошибок является мощнейшим способом, который увеличивает устойчивость вашего кода. Перекрытие ошибок является фундаментальной концепцией для каждой написанной вами программы, но это особенно важно в Java, где одна из главнейших целей — это создание компонент программ для других. Для создание помехоустойчивой системы каждый компонент должен быть помехоустойчивым.
Цель обработки исключений в Java состоит в упрощении создания больших, надежных программ при использовании меньшего кода, насколько это возможно, и с большей уверенностью, что ваше приложение не имеет не отлавливаемых ошибок.
Исключения не ужасно сложны для изучения и это одна из тех особенностей, которая обеспечивает немедленную и значительную выгоду для вашего проекта. К счастью, Java ограничивает все аспекты исключений, так что это гарантирует, что они будут использоваться совместно и разработчиком библиотеки, и клиентским программистом.
Упражнения
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
- Создайте класс с main( ) , который выбрасывает объект, класса Exception внутри блока try . Передайте конструктору Exception аргумент String . Поймайте исключение внутри предложение catch и напечатайте аргумент String . Добавьте предложение finally и напечатайте сообщение, чтобы убедится, что вы были там.
- Создайте ваш собственный класс исключений, используя ключевое слово extends . Напишите конструктор для этого класса, который принимает аргумент String , и хранит его внутри объекта в ссылке String . Напишите метод, который печатает хранящийся String . Создайте предложение try-catch для наблюдения своего собственного исключения.
- Напишите класс с методом, который выбрасывает исключение типа, созданного в Упражнении 2. Попробуйте откомпилировать его без спецификации исключения, чтобы посмотреть, что скажет компилятор. Добавьте соответствующую спецификацию исключения. Испытайте ваш класс и его исключение в блоке try-catch.
- Определите ссылку на объект и инициализируйте ее значением null . Попробуйте вызвать метод по этой ссылке. Не окружайте код блоком try-catch , чтобы поймать исключение.
- Создайте класс с двумя методами f( ) и g( ) . В g( ) выбросите исключение нового типа, который вы определили. В f( ) вызовите g( ) , поймайте его исключение и, в предложении catch , выбросите другое исключение (второго определенного вами типа). Проверьте ваш код в main( ) .
- Создайте три новых типа исключений. Напишите класс с методом, который выбрасывает все три исключения. В main( ) вызовите метод, но используйте только единственное предложение catch , которое будет ловить все три вида исключений.
- Напишите код для генерации и поимки ArrayIndexOutOfBoundsException .
- Создайте свое собственное поведение по типу возобновления, используя цикл while , который будет повторяться, пока исключение больше не будет выбрасываться.
- Создайте трехуровневую иерархию исключений. Теперь создайте базовый класс A , с методом, который выбрасывает исключение базового класса вашей иерархии. Наследуйте B от A и перегрузите метод так, чтобы он выбрасывал исключение второго уровня в вашей иерархии. Повторите то же самое, унаследовав класс C от B . В main( ) создайте C и приведите его к A , затем вызовите метод.
- Покажите, что конструктор наследуемого класса не может ловить исключения, брошенные конструктором базового класса .
- Покажите, что OnOffSwitch.java может завершиться неудачей при выбрасывании RuntimeException внутри блока try .
- Покажите, что WithFinally.java не завершится неудачей при выбрасывании RuntimeException в блоке try .
- Измените Упражнение 6, добавив предложение finally . Проверьте, что предложение finally выполняется даже, если выбрасывается NullPointerException .
- Создайте пример, в котором вы используете флаг для управления вызовом кода очистки, как описано во втором параграфе под заголовком “Конструкторы”.
- Измените StormyInning.java , добавив тип исключения UmpireArgument и метод, который его выбрасывает. Проверьте измененную иерархию.
- Удалите первый catch в Human.java и проверьте, что код все равно компилируется и правильно работает.
- Добавьте второй уровень потерь исключения в LostMessage.java , так чтобы HoHumException заменялось третьим исключением.
- В Главе 5 найдите две программы, называемые Assert.java и измените их, чтобы они выбрасывали свои собственные исключения вместо печать в System.err . Это исключение должно быть внутренним классом, расширяющим RuntimeException .
- Добавьте подходящий набор исключений в c08:GreenhouseControls.java .
[51] C программист может посмотреть на возвращаемое значение printf( ) , как пример этого.
[52] Это значительное улучшение, по сравнению с обработкой исключений в C++, которая не ловит нарушения спецификации исключений до времени выполнения, хотя это не очень полезно.
[53] Обработка исключений в C++ не имеет предложения finally , поэтому в C++ освобождение происходит в деструкторах, чтобы завершить такой род очистки.
[54] Деструктор — это функция, которая всегда вызывается, когда объект более не используется. Вы всегда знаете точно, где совершен вызов деструктора. C++ имеет автоматический вызов деструктора, но Object Pascal из Delphi версии 1 и 2 не делает этого (что изменяет значение и использование концепции деструкторов в этом языке).
[55] ISO C++ добавил сходное ограничение, которое требует, чтобы исключение наследуемого метода были теми же или наследовались от тех же, что и выбрасываемые методом базового класса. Это первый случай, в котором C++ реально способен проверить спецификацию исключений во время компиляции.
[56] В C++ деструктор должен это обрабатывать за вас.
Источник
Афоризм
С тех пор прошло два года… високосных.
Поддержка проекта
Если Вам сайт понравился и помог, то будем признательны за Ваш «посильный» вклад в его поддержку и развитие
• Yandex.Деньги
410013796724260
• Webmoney
R335386147728
Z369087728698
1. Понятие «Исключение» |
2. Операторы исключений |
3. Оператор throws |
4. Блоки кода try/catch и try/finally |
5. Может ли блок finally не выполняться? |
6. Проверяемые и непроверяемые исключения |
7. Возбуждение исключения |
8. Определение исключения в сигнатуре метода |
9. Особенность RuntimeException |
10. Возбуждение исключения в методе main |
11. Множественные исключения |
12. Последовательность нескольких блоков catch |
13. Поглащение исключений в блоке try…finally |
14. Исключение SQLException |
15. Ошибка Error |
16. Обобщение исключений |
17. Логирование исключений |
Вопросы и ответы для собеседование по Java, Содержание.
Вопросы и ответы для собеседование по Java, часть 1.
Вопросы и ответы для собеседование по Java, часть 2.
Вопросы и ответы для собеседование по Java, часть 4.
Вопросы и ответы для собеседование по Java, часть 5.
Вопросы и ответы для собеседование по Java, часть 6.
1. Понятие «Исключение»
Исключение — это ошибка, возникающая во время выполнения программы. Причины возникновения исключения
могут разные, например :
- некорректно определены (не определены) данные;
- невозможно прочитать или создать файл;
- обрыв сетевого соединения или соединения с сервером базы данных.
Исключение в Java является объектом. Поэтому они могут не только создаваться автоматически виртуальной
машиной JVM при возникновении исключительной ситуации, но и порождаться самим разработчиком.
2. Операторы исключений
Java имеет пять ключевых операторов для определения блока исключений, перехвата и возбуждения исключений :
- try — начала блока кода, в котором может возникнуть исключение, и которое следует перехватить;
- catch — начала блока кода, предназначенного для перехвата и обработки исключений (параметром catch
является тип ожидаемого исключения); - throw — оператор для генерации исключений;
- throws — ключевое слово, используемое в сигнатуре метода, и обозначающее, что метод
потенциально может вызвать исключение с определенным типом; - finally — начала дополнительного блока кода, размещаемый после последнего блока catch. Блок finally
не является обязательным, но всегда получает управление.
Общая структура «перехвата» исключительной ситуации выглядит следующим образом :
try { // код программы, который может привести к ошибке } catch(Exception e ) { // код программы для обработки исключений } finally { // выполнение блока программы независимо от наличия исключения }
3. Оператор throws
Оператор throws включается в сигнатуру метода с целью обозначения возожности возникновения
исключительной ситуации с определенным типом. Использовать данный оператор следует в описании тех методов,
которые могут возбуждать исключения, но сами их не обрабатывают. Таким образом, оператором throws метод
предупреждает другие методы, вызывающие данный, что у него могут быть вызваны необработанные исключения,
чтобы вызывающие методы могли защитить себя от этих исключений.
public class TestThrow { static void method() throws IllegalAccessException { System.out.println("inside method"); // . . . throw new IllegalAccessException ("Exception in method"); } public static void main(String args[]) { try { method(); } catch(IllegalAccessException e) { System.out.println("Catch inside main : " + e.getMessage()); } } }
4. Блоки кода try/catch и try/finally
Каждый оператор try требует наличия либо catch, либо finally, либо сочетания catch и finally. Блок кода finally
не является обязательным, и может отсутствовать при использовании try/catch. Оператором finally создаётся блок кода,
который должен быть выполнен после завершения блока try/catch.
Если необходимо гарантировано выполнить определенный участок кода, то используется finally. Связка try/finally
позволяет обеспечить выполнение блока кода независимо от того, какие исключения были возбуждены и перехвачены,
даже в тех случаях, когда в методе нет соответствующего возбужденному исключению раздела catch.
Пример использования try/finally представлен здесь.
5. Может ли блок finally не выполняться?
Код блока finally не будет исполнен, если в код программы включен предшествующий блоку finally системный выход.
Следующий пример демонстрирует данную ситуацию.
try { System.exit(0); } catch(Exception e) { System.err.println(e.getMessage()); } finally { // код блока }
6. Проверяемые и непроверяемые исключения
Все исключения делятся на «проверяемые» (checked) и «непроверяемые» (unchecked). Данное свойство присуще
базовому классу исключения Throwable и передается по наследству (Error, Exception, RuntimeException). В исходном
коде класса исключения данное свойство недоступно. Ниже представлена иерархия классов исключений.
Object | Throwable(CHECKED) / Error(UNCHECKED) Exception(CHECKED) | RuntimeException(UNCHECKED)
Исключения Throwable и Exception, а также все их наследники, за исключением Error и RuntimeException,
являются «проверяемыми» checked исключениями. Error и RuntimeException, а также все их наследники, относятся
к «непроверяемым» unchecked исключениям.
Проверка исключения на checked выполняется компилятором (compile-time checking). Непроверяемые исключения
можно перехватить (catch) в момент исполнения программы (runtime checking).
Java – это язык программирования со статической типизацией, т.е. его компилятор отслеживает корректности
использования типов : наличие полей и методов, checked исключения, и т.д. Следующий пример демонстрирует наличие
двух ошибок программирования, определенные на этапе копиляции.
public class TestException { public static Double divide(int i1, int i2) throws Exception { if (i2 != 0) return new Double (i2 / i2); else { Throwable e = new Exception(); throw e; // Unhandled exception type Throwable } } public static void main(String[] args) { Object obj = "Hello!"; char c = obj.charAt(0); // The method charAt(int) is // undefined for the type Object } }
Сигнатура метода divide (операция деления) включает определение возможного исключения типа Exception, наследуемого
от Throwable. Если делитель (i2) равен 0, то создается объект исключения типа Throwable и возбуждается исключение
(throw t), которое не пропускает компилятор, т.к. тип возбуждаемого исключения не соответствует типу исключения в
сигнатуре метода. Если в сигнатуре метода определить исключение типа Throwable, то ошибки не будет.
В методе main определен объект obj с инициализацией определенным значением. В следующей строке компилятор находит
ошибку, связанную с отсутствием метода charAt(int) в объекте типа Object. Если выполнить приведение типа obj к String,
то компилятор пропустит код : char c = ((String)ref).charAt(0).
Пример unchecked исключения представлен здесь.
7. Возбуждение исключения
Как было отмечено выше, исключение является объектом, который можно создать программно. Чтобы возбудить (выбросить)
исключение используется оператор throw.
Exception e = new SQLException(); throw e;
8. Определение исключения в сигнатуре метода
В сигнатуре метода можно определить возможное проверяемое (checked) исключение. Следующий
пример демонстрирует определение исключения в сигнатуре метода f1() и его перехват в конструкторе класса.
import java.io.EOFException; import java.io.FileNotFoundException; public class TestException { public TestException() { try { f1(); } catch (FileNotFoundException e) { e.printStackTrace(); } } public void f1() throws FileNotFoundException { throw new FileNotFoundException(); } public static void main(String[] args) { new TestException (); } }
9. Особенность RuntimeException
Исключение RuntimeException расширяет свойства Exception и является базовым классом для ошибок во время выполнения
приложения. Данное исключение относится к необрабатываемым исключениям (unchecked). Согласно описанию класса это
исключение может возникнуть во время нормальной работы JVM.
Следующий код демонстрирует пример использования непроверяемого исключения NumberFormatException (наследующего
свойства RuntimeException). В функции parseInt при преобразовании строкового значения в число в режиме run-time может
возникнуть исключение. Можно метод функции Integer.parseInt() «обернуть» в try/catch, а можно передать обработку
исключения функции в вызывающий метод, для чего в сигнатуре определяется соответствующее исключение (throws).
public int parseInt(String s) throws NumberFormatException { return Integer.parseInt(s); }
10. Возбуждение исключения в методе main
Если в методе main возбудить исключение, то оно будет передано в виртуальную машину Java (JVM).
11. Множественные исключения
В сигнатуре метода можно определить несколько возможных исключений. Для этого используется оператор throws
и исключения, разделенные запятыми. Следующий пример демонстрирует метод callMethods с множественными
возможными исключениями :
import java.io.EOFException; import java.io.FileNotFoundException; public class TestException { public void callMethods() throws EOFException, FileNotFoundException { if (f1()) f0(); } public void f0() throws EOFException { // ... throw new EOFException(); } public boolean f1() throws FileNotFoundException { // ... throw new FileNotFoundException(); } }
Чтобы перехватить несколько возможных исключений можно искользовать конструкцию try с несколькими catch.
try { if (f1()) f0(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (EOFException e) { e.printStackTrace(); }
12. Последовательность нескольких блоков catch
При определение нескольких блоков catch следует руководствоваться правилом обработки исключений от «младшего»
к старшему. Т.е. нельзя размещать первым блоком catch (Exception e) {…}, поскольку все остальные блоки catch()
уже не смогут перехватить исключение. Помните, что Exception является базовым классом, поэтому его стоит размещать
последним.
Рассмотрим следующую иерархию наследования исключений :
java.lang.Object java.lang.Throwable java.lang.Exception java.io.IOException java.io.EOFException
Cамым младшим исключением является EOFException, поэтому он должен располагаться перед IOException и Exception,
если используется несколько блоков catch с данными типами исключений. Следующий код является демонстрацией
данного принципа.
String x = "Hello"; try { if (!x.equals("Hello")) throw new IOException(); else throw new EOFException(); } catch (EOFException e) { System.err.println("EOFException : " + e.getMessage()); } catch (IOException e) { System.err.println("IOException : " + e.getMessage()); } catch (Exception e) { System.err.println("Exception : " + e.getMessage()); }
13. Поглащение исключений в блоке try…finally
Если было вызвано два исключения — одно в блоке try, а второе в finally — то, при отсутствии catch,
исключение в finally «проглотит» предыдущее исключение. Следует блоки с возможными исключениями всегда
обрамлять операторами try/catch, чтобы не потерять важную информацию. Следующий пример демонстрирует
«поглащение» исключения в блоке try новым исключением в блоке finally.
public class TestException { public TestException() { try { System.out.println(absorbingEx()); } catch (EOFException e) { System.out.println(e.getMessage()); } catch (IOException e) { System.out.println(e.getMessage()); } } public String absorbingEx() throws IOException, EOFException { try { throw new EOFException("EOFException"); // } catch (EOFException e) { // System.out.println("catch " + e.getMessage()); } finally { throw new IOException("finally IOException"); } } public static void main(String[] args) { new TestException(); System.exit(0); } }
В результате в консоль будет выведено следующее сообщение :
Чтобы не «потерять» исключение, необходимо его корректно перехватить и обработать. В примере следует
убрать комментарий с блока catch.
14. Исключение SQLException
Исключение SQLException связано с ошибками при работе с базой данных. Данное исключением относится
к checked исключениям, и, следовательно, проверяется на этапе компиляции.
java.lang.Object java.lang.Throwable java.lang.Exception java.sql.SQLException
Споры вокруг SQLException связаны с тем, что исключение возникает во время исполнения, а обрабатывать
его приходится в коде, чтобы не ругался компилятор; может быть следовало бы отнести его к unchecked
run-time исключениям? Убедительный довод разработчиков данного исключения связан с тем, что необходимо
программисту обработать свои возможные ошибки при работе с базой данных.
15. Ошибка Error
Ошибка Error относится к подклассу не проверяемых (unchecked) исключений, которая показывает серьезные
проблемы, возникающие во время выполнения программы. Большинство из ошибок данного класса сигнализируют о
ненормальном ходе выполнения программы, т.е. о возникновении критических проблем.
Согласно спецификации Java, не следует пытаться обрабатывать Error в собственной программе, поскольку
они связаны с проблемами уровня JVM. Исключения такого рода возникают, если, например,
закончилась память, доступная виртуальной машине.
16. Обобщение исключений
При определении в сигнатуре метода возможных исключений можно вместо нескольких проверяемых исключений
указать общее (базовое) java.lang.Throwable. В этом случае, компилятор «пропустит код» и программа возможно
отработает без сбоев. Например :
public void callCommonException() throws Exception { Object obj = new Object(); method(obj); } public void method(Object obj) throws NumberFormatException, IllegalArgumentException { // ... }
Использование Exception или Throwable в сигнатуре метода делает почти невозможным правильное обращение с
исключениями при вызове метода. Вызывающий метод получает только информацию о том, что что-то может отработать
некорректно.
Перехват обобщенных исключений не позволяет «решить» проблему, т.е. «вылечить» программу, а помогает только
обойти периодически возникающую проблему. Это может привести к нескольким непредвиденным ошибкам в других местах
приложения.
17. Логирование исключений
Лучше всего логировать (регистрировать) исключение в месте его обработки. Это связано с тем, что именно
в данном месте кода достаточно информации для описания возникшей проблемы. Кроме этого, одно и то же исключение
при вызове одного и того же метода можно перехватывать в разных местах программы; регистрировать же следует в
одном месте.
Иногда исключение может быть частью ожидаемого поведения. В этом случае нет необходимости его регистрировать.
Вопросы и ответы для собеседование по Java, Содержание.
Вопросы и ответы для собеседование по Java, часть 1.
Вопросы и ответы для собеседование по Java, часть 2.
Вопросы и ответы для собеседование по Java, часть 4.
Вопросы и ответы для собеседование по Java, часть 5.
Вопросы и ответы для собеседование по Java, часть 6.
В нашей жизни нередко возникают ситуации, которые мы не планировали. К примеру, пошли вы утром умываться и с досадой обнаружили, что отключили воду. Вышли на улицу, сели в машину, а она не заводится. Позвонили другу, а он недоступен. И так далее и тому подобное… В большинстве случаев человек без труда справится с подобными проблемами. А вот как с непредвиденными ситуациями справляется Java, мы сейчас и поговорим.
Что называют исключением. Исключения в мире программирования
В программировании исключением называют возникновение ошибки (ошибок) и различных непредвиденных ситуаций в процессе выполнения программы. Исключения могут появляться как в итоге неправильных действий юзера, так и из-за потери сетевого соединения с сервером, отсутствии нужного ресурса на диске и т. п. Также среди причин исключений — ошибки программирования либо неверное использование API.
При этом в отличие от «человеческого мира», программное приложение должно чётко понимать, как поступать в подобной ситуации. И вот как раз для этого в Java и существует механизм исключений (exception).
Используемые ключевые слова
При обработке исключений в Java применяются следующие ключевые слова:
— try – служит для определения блока кода, в котором может произойти исключение;
— catch – необходим для определения блока кода, где происходит обработка исключения;
— finally – применяется для определения блока кода, являющегося необязательным, однако при его наличии он выполняется в любом случае вне зависимости от результата выполнения блока try.
Вышеперечисленные ключевые слова необходимы для создания в коде ряда специальных обрабатывающих конструкций: try{}finally{}, try{}catch, try{}catch{}finally.
Кроме того:
1. Для возбуждения исключения используем throw.
2. Для предупреждения в сигнатуре методов о том, что метод может выбросить исключение, применяем throws.
Давайте на примере посмотрим, как используются ключевые слова в Java-программе:
//метод считывает строку с клавиатуры public String input() throws MyException {//предупреждаем с помощью throws, // что метод может выбросить исключение MyException BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String s = null; //в блок try заключаем код, в котором может произойти исключение, в данном // случае компилятор нам подсказывает, что метод readLine() класса // BufferedReader может выбросить исключение ввода/вывода try { s = reader.readLine(); // в блок catch заключаем код по обработке исключения IOException } catch (IOException e) { System.out.println(e.getMessage()); // в блоке finally закрываем поток чтения } finally { // при закрытии потока тоже возможно исключение, например, если он не был открыт, поэтому “оборачиваем” код в блок try try { reader.close(); // пишем обработку исключения при закрытии потока чтения } catch (IOException e) { System.out.println(e.getMessage()); } } if (s.equals("")) { // мы решили, что пустая строка может нарушить в дальнейшем работу нашей программы, например, на результате этого метода нам надо вызывать метод substring(1,2), поэтому мы вынуждены прервать выполнение программы с генерацией своего типа исключения MyException с помощью throw throw new MyException("String can not be empty!"); } return s; }Зачем нам механизм исключений?
Для понимания опять приведём пример из обычного мира. Представьте, что на какой-нибудь автодороге имеется участок с аварийным мостом, на котором ограничена грузоподъёмность. И если по такому мосту проедет грузовик со слишком большой массой, мост разрушится, а в момент этого ЧП ситуация для шофёра станет, мягко говоря, исключительной. И вот, дабы такого не произошло, дорожные службы заранее устанавливают на дороге соответствующие предупреждающие знаки. И тогда водитель, посмотрев на знак, сравнит массу своего авто со значением разрешённой грузоподъёмности и примет соответствующее решение, например, поедет по другой дороге.
То есть мы видим, что из-за правильных действий дорожной службы шоферы крупногабаритных транспортных средств:
1) получили возможность заранее изменить свой путь;
2) были предупреждены об опасности;
3) были предупреждены о невозможности проезжать по мосту при определённых условиях.Вот как наш жизненный пример соотносится с применением исключения на Java:
Исходя из вышесказанного, мы можем назвать одну из причин применения исключений в Java. Заключается она в возможности предупреждения исключительной ситуации для её последующего разрешения и продолжения работы программы. То есть механизм исключений позволит защитить написанный код от неверного применения пользователем путём валидации входящих данных.
Что же, давайте ещё раз побудем дорожной службой. Чтобы установить знак, мы ведь должны знать места, где водителей ТС могут ждать различные неприятности. Это первое. Далее, нам ведь надо заготовить и установить знаки. Это второе. И, наконец, надо предусмотреть маршруты объезда, позволяющие избежать опасности.
В общем, механизм исключений в Java работает схожим образом. На стадии разработки программы мы выполняем «ограждение» опасных участков кода в отношении наших исключений, используя блок try{}. Чтобы предусмотреть запасные пути, применяем блок catch{}. Код, выполняемый в программе при любом исходе, пишем в блоке finally{}.
Иногда бывает, что мы не можем предусмотреть «запасной аэродром» либо специально желаем предоставить право его выбора юзеру. Но всё равно мы должны как минимум предупредить пользователя об опасности. Иначе он превратится в разъярённого шофёра, который ехал долго, не встретил ни одного предупреждающего знака и в итоге добрался до аварийного моста, проехать по которому не представляется возможным.
Что касается программирования на Java, то мы, когда пишем свои классы и методы, далеко не всегда можем предвидеть контекст их применения другими программистами в своих программах, а значит, не можем со стопроцентной вероятностью предвидеть правильный путь для разрешения исключительных ситуаций. Но предупредить коллег о возможной исключительной ситуации мы всё-таки должны, и это не что иное, как правило хорошего тона.
Выполнить это правило в Java нам как раз и помогает механизм исключений с помощью throws. Выбрасывая исключение, мы, по сути, объявляем общее поведение нашего метода и предоставляем пользователю метода право написания кода по обработке исключения.
Предупреждаем о неприятностях
Если мы не планируем обрабатывать исключение в собственном методе, но желаем предупредить пользователей метода о возможной исключительной ситуации, мы используем, как это уже было упомянуто, ключевое слово throws. В сигнатуре метода оно означает, что при некоторых обстоятельствах метод может выбросить исключение. Это предупреждение становится частью интерфейса метода и даёт право пользователю на создание своего варианта реализации обработчика исключения.
После упоминания ключевого слова throws мы указываем тип исключения. Как правило, речь идёт о наследниках класса Exception Java. Но так как Java — это объектно-ориентированный язык программирования, все исключения представляют собой объекты.
Иерархия исключений в Java
Когда возникают ошибки при выполнении программы, исполняющая среда Java Virtual Machine обеспечивает создание объекта нужного типа, используя иерархию исключений Java — речь идёт о множестве возможных исключительных ситуаций, которые унаследованы от класса Throwable — общего предка. При этом исключительные ситуации, которые возникают в программе, делят на 2 группы:
1. Ситуации, при которых восстановление нормальной дальнейшей работы невозможно.
2. Ситуации с возможностью восстановления.К первой группе можно отнести случаи, при которых возникают исключения, которые унаследованы из класса Error. Это ошибки, возникающие во время выполнения программы при сбое работы Java Virtual Machine, переполнении памяти либо сбое системы. Как правило, такие ошибки говорят о серьёзных проблемах, устранение которых программными средствами невозможно. Данный вид исключений в Java относят к неконтролируемым исключениям на стадии компиляции (unchecked). К этой же группе относятся и исключения-наследники класса Exception, генерируемые Java Virtual Machine в процессе выполнения программы — RuntimeException. Данные исключения тоже считаются unchecked на стадии компиляции, а значит, написание кода по их обработке необязательно.
Что касается второй группы, то к ней относят ситуации, которые можно предвидеть ещё на стадии написания приложения, поэтому для них код обработки должен быть написан. Это контролируемые исключения (checked). И в большинстве случаев Java-разработчики работают именно с этими исключениями, выполняя их обработку.
Создание исключения
В процессе исполнения программы исключение генерируется Java Virtual Machine либо вручную посредством оператора throw. В таком случае в памяти происходит создание объекта исключения, выполнение основного кода прерывается, а встроенный в JVM обработчик исключений пробует найти способ обработать это самое исключение.
Обработка исключения
Обработка исключений в Java подразумевает создание блоков кода и производится в программе посредством конструкций try{}finally{}, try{}catch, try{}catch{}finally.
В процессе возбуждения исключения в try обработчик исключения ищется в блоке catch, который следует за try. При этом если в catch присутствует обработчик данного вида исключения, происходит передача управления ему. Если же нет, JVM осуществляет поиск обработчика данного типа исключения, используя для этого цепочку вызова методов. И так происходит до тех пор, пока не находится подходящий catch. После того, как блок catch выполнится, управление переходит в необязательный блок finally. Если подходящий блок catch найден не будет, Java Virtual Machine остановит выполнение программы, выведя стек вызовов методов под названием stack trace. Причём перед этим выполнится код блока finally при наличии такового.
Рассмотрим практический пример обработки исключений:
public class Print { void print(String s) { if (s == null) { throw new NullPointerException("Exception: s is null!"); } System.out.println("Inside method print: " + s); } public static void main(String[] args) { Print print = new Print(); List list= Arrays.asList("first step", null, "second step"); for (String s:list) { try { print.print(s); } catch (NullPointerException e) { System.out.println(e.getMessage()); System.out.println("Exception was processed. Program continues"); } finally { System.out.println("Inside bloсk finally"); } System.out.println("Go program...."); System.out.println("-----------------"); } } }А теперь глянем на результаты работы метода main:
Inside method print: first step Inside bloсk finally Go program.... ----------------- Exception: s is null! Exception was processed. Program continues Inside bloсk finally Go program.... ----------------- Inside method print: second step Inside bloсk finally Go program.... -----------------Блок finally чаще всего используют, чтобы закрыть открытые в try потоки либо освободить ресурсы. Но при написании программы уследить за закрытием всех ресурсов возможно не всегда. Чтобы облегчить жизнь разработчикам Java, была предложена конструкция try-with-resources, автоматически закрывающая ресурсы, открытые в try. Используя try-with-resources, мы можем переписать наш первый пример следующим образом:
public String input() throws MyException { String s = null; try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))){ s = reader.readLine(); } catch (IOException e) { System.out.println(e.getMessage()); } if (s.equals("")){ throw new MyException ("String can not be empty!"); } return s; }А благодаря появившимся возможностям Java начиная с седьмой версии, мы можем ещё и объединять в одном блоке перехват разнотипных исключений, делая код компактнее и читабельнее:
public String input() { String s = null; try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { s = reader.readLine(); if (s.equals("")) { throw new MyException("String can not be empty!"); } } catch (IOException | MyException e) { System.out.println(e.getMessage()); } return s; }Итоги
Итак, применение исключений в Java повышает отказоустойчивость программы благодаря использованию запасных путей. Кроме того, появляется возможность отделить код обработки исключительных ситуаций от логики основного кода за счёт блоков catch и переложить обработку исключений на пользователя кода посредством throws.
Основные вопросы об исключениях в Java
1.Что такое проверяемые и непроверяемые исключения?
Если говорить коротко, то первые должны быть явно пойманы в теле метода либо объявлены в секции throws метода. Вторые вызываются проблемами, которые не могут быть решены. Например, это нулевой указатель или деление на ноль. Проверяемые исключения очень важны, ведь от других программистов, использующих ваш API, вы ожидаете, что они знают, как обращаться с исключениями. К примеру, наиболее часто встречаемое проверяемое исключение — IOException, непроверяемое — RuntimeException.
2.Почему переменные, определённые в try, нельзя использовать в catch либо finally?
Давайте посмотрим на нижеследующий код. Обратите внимание, что строку s, которая объявлена в блоке try, нельзя применять в блоке catch. То есть данный код не скомпилируется.try { File file = new File("path"); FileInputStream fis = new FileInputStream(file); String s = "inside"; } catch (FileNotFoundException e) { e.printStackTrace(); System.out.println(s); }А всё потому, что неизвестно, где конкретно в try могло быть вызвано исключение. Вполне вероятно, что оно было вызвано до объявления объекта.
3.Почему Integer.parseInt(null) и Double.parseDouble(null) вызывают разные исключения?
Это проблема JDK. Так как они были разработаны разными людьми, то заморачиваться вам над этим не стоит:Integer.parseInt(null); // вызывает java.lang.NumberFormatException: null Double.parseDouble(null); // вызывает java.lang.NullPointerException4.Каковы основные runtime exceptions в Java?
Вот лишь некоторые из них:IllegalArgumentException ArrayIndexOutOfBoundsExceptionИх можно задействовать в операторе if, если условие не выполняется:
if (obj == null) { throw new IllegalArgumentException("obj не может быть равно null");5.Возможно ли поймать в одном блоке catch несколько исключений?
Вполне. Пока классы данных исключений можно отследить вверх по иерархии наследования классов до одного и того же суперкласса, возможно применение только этого суперкласса.
6.Способен ли конструктор вызывать исключения?
Способен, ведь конструктор — это лишь особый вид метода.class FileReader{ public FileInputStream fis = null; public FileReader() throws IOException{ File dir = new File(".");//get current directory File fin = new File(dir.getCanonicalPath() + File.separator + "not-existing-file.txt"); fis = new FileInputStream(fin); } }7.Возможен ли вызов исключений в final?
В принципе, можете сделать таким образом:public static void main(String[] args) { File file1 = new File("path1"); File file2 = new File("path2"); try { FileInputStream fis = new FileInputStream(file1); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { FileInputStream fis = new FileInputStream(file2); } catch (FileNotFoundException e) { e.printStackTrace(); } } }Но если желаете сохранить читабельность, объявите вложенный блок try-catch в качестве нового метода и вставьте вызов данного метода в блок finally.
finally. public static void main(String[] args) { File file1 = new File("path1"); File file2 = new File("path2"); try { FileInputStream fis = new FileInputStream(file1); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { methodThrowException(); } }