Исключения

Причиной появления исключений, на мой взгляд, стало очень простое предположение — во время исполнения программы вряд ли можно быть уверенным, что все функции будут делать свою работу без сбоев. Например, если ваш код должен прочитать файл с каким-то именем, а файла такого просто нет ? Или вы хотите по сети обратиться к другой программе, но добрая старушка-уборщица выдернула шнур из компьютера ? В конце концов ваш калькулятор при вычислении дроби получил в знаменателе ноль. Да много ли каких еще препятствий может встретиться на пути.
Конечно, не все методы/функции обязательно будут оканчиваться крахом, но это обычное дело. И что в этом случае необходимо предпринять ? В старое доброе время в языке Си было принято возвращать какое-то значение. Причем в разных случаях тот же ноль мог означать что все хорошо, но мог означать что все плохо. В общем стандарта как не было, так до сих пор и нет — что должна возвращать функция в случае возникновения ошибки. Определенно можно сказать только одно — функция должна возвращать:

а) признак того, что произошла ошибка
б) информацию о том, а что собственно плохого произошло

С пунктом б) как раз достаточно просто — у нас же ООП. Значит удобно всю необходимую информацию поместить в какой-либо объект какого-то класса и вернуть. В этом объекте мы можем развернуться по полной — и сообщение написать и код ошибки выдать и много чего еще.
Что касается пункта а), то несложно догадаться, что возвращать ошибку как результат выполнения функции — плохая идея. Если мы ожидаем целое число, а ошибка будет описываться пусть даже просто строкой, то вряд ли кто-то сможет предложить что-то более-менее приемлемое. Но идея возвращать объект-описание ошибки — красивая и благодарная идея.
Отсюда родилась следующее решение — метод может генерировать исключение (объект-ошибку) и завершаться не вызовом return в случае ошибки, а иначе — метод будет «бросать» исключение.
И тогда код, вызывающий такой метод может получить ответ двумя способами:

  1. Обычным возвратом значения из метода, если все хорошо
  2. Будет «ловить» исключение

Соответственно метод, который хочет возвращать ошибку, тоже должен уметь возвращать какое-то значение (или ничего не возвращать в случае void) когда все хорошо и создавать и «бросать» исключения, если что-то пошло не так. Я уже дважды использовал термин «бросать», причем не без умысла — в языке Java используется глагол throw (бросать, кидать), посему больше не буду брать его в кавычки.
Мы получаем три момента для рассмотрения:

  1. Описание объекта-исключения — его же надо уметь создавать
  2. Описание метода, который умеет бросать исключения
  3. Как правильно ловить исключение

Итак, еще раз — надо создать исключение, надо правильно бросить исключение, надо правильно поймать исключение.

Класс для исключения

Научимся создавать исключения. Для таких классов существует специальная иерархия классов, которая начинается с класса java.lang.Throwable. (Надеюсь вы помните, что java.lang означет пакет, в котором находится описание класса. По сути директория). Если перевести это название получится что-то вроде «готовый к бросанию» — с литературным переводом возможно у меня не очень красиво получилось, но идея именно такая: объекты этого класса (и всех потомков) могут быть брошены. Этот класс редко используется для прямого наследования, чаще используется его потомок, класс java.lang.Exception. А уж у этого класса «детишек» очень много. Как вы наверно уже догадались, количество готовых классов для Java огромно. Вряд ли конечно на каждого человека на земле приходится один класс, хотя возможно я не так уж и далек от истины. В общем, уже готовых классов-исключений среди других много. Так что будет что изучать. Но о них мы поговорим несколько позже, а пока все-таки создадим наш класс-исключение.

Как вы можете видеть ничего особенного в описании нет — создали класс, унаследовали его от класса Exception, определили свой конструктор, переопределили уже существующий — в общем ничего неординарного и загадочного. Единственное, что хотелось бы отметить — это наличие у класса Throwable (предка Exception) нескольких достаточно востребованных методов.

getMessage() — получить сообщение об ошибке, которое обычно имеет смысл читать
printStackTrace() — распечатать полный стек вызовов. Стек вызовов — это полный список всех методов внутри которых случилась ошибка. Т.е. если вызывался method1, внутри него method2, потом method3 и в нем случилось исключение, то вы увидите все три метода. Чуть позже мы с вами посмотрим пример использования этого метода. Не пренебрегайте им — он очень удобный и информативный

Метод для генерации исключения

Генерировать исключение можно в методе и этот метод может объявить, что он кидает исключения определенного класса. Делается это достаточно просто — после списка аргументов (в скобках) пишется ключевое слово throws и через запятую перечисляются классы исключений, который может порождать данный метод. Потом открывается фигурная скобка и мы пишем тело метода. Давайте посмотрим пример такого описания — наш класс Generator включает метод helloMessage который принимает в качестве строки имя, чтобы отдать строку «Hello, <имя>«. Но если имя не указано (указатель на строку равен null), то метод не возвращает например пустую строку — он кидает исключение, которое можно использовать в дальнейшей логике.

Опять смотрим код и видим, что после проверки на null мы создаем объект-исключение (обратите внимание — мы просто создаем нужный нам объект, заполняем его нужными значениями (мы в нем описываем реальную проблему, которая произошла) и кидаем. Опять же обращаю ваше внимание на то, что мы не делаем вызов return — этот вызов делается в случае если все хорошо. Мы кидаем исключение — пишем специальную конструкцию throw (не перепутайте — в описании метода пишем глагол в третьем лице единственного числа по английской грамматике с окончанием s — throws — «бросает», а в коде используем повелительное наклонение — throw — «бросай»).
Несколько позже мы рассмотрим еще несколько моментов, которые касаются описания методов, которые бросают исключения. Пока же перейдем к третьему шагу — как поймать исключение.

Ловим исключение

Для того, чтобы поймать исключение используется конструкция try … catch. Перед блоком кода, который порождает исключение пишем слово try и открываем фигурные скобки. После окончания блока закрываем скобки, пишем слово catch, в скобках указываем переменную класса-исключения, открываем скобки и там пишем код, который будет вызываться ТОЛЬКО в случае если метод/методы внутри try породили исключение. Смотрим код:

Полный текст проекта можно скачать тут — SimpleException. При запуске можно увидеть, что первый кусочек try … catch выкинет исключение и мы увидим текст об ошибках

Error code:10
Error message:Message is null

Второй же пройдет гладко и мы увидим приветствие для самого известного джедая

Answer 2:Hello, Yoda

Обратите внимание, что в первом блоке try … catch строка «Answer 1:» не выводится. Т.е. поведение при генерации исключения следующее: сразу после создания исключения все остальные строки внутри блока уже не выполняются и вы сразу перемещаетесь в блок catch. Думаю, что это достаточно очевидно.
Итак, мы рассмотрели простой вариант использования исключений, но это конечно же не все. Так что продолжим.

Блок finally

Как мы видели чуть выше, если во время исполнения внутри блока try … catch случается исключение, то все оставшиеся строки НЕ ВЫПОЛНЯЮТСЯ. Но это не всегда полностью соответствует нашим желаниям. Существует немалое количество ситуаций, когда, чтобы не случилось, какие-то строки кода должны выполняться вне зависимости от того, каков результат. Именно для этого существует секция finally.
Например, вы открыли файл на запись и долгое время что-то туда записывали. Но наступил момент, когда какие-то данные по определенным причинам туда не попали и запиcь надо прекращать. Но даже в этой ситуации файл нужно сохранить (закрыть). Или вы открыли сетевое соединение, которое в любом случае надо закрыть. Наконец вам просто надо всегда обнулить какой-то счетчик в конце. Давайте посмотрим структуру с finally

Если вы запустите наш пример, то сможете увидеть, что вывод на печать в секции finally вызывается в обоих случаях.

Множество исключений

Как вы наверно уже догадываетесь, метод может порождать не одно исключение, а много. Тем более, если у вас внутри блока try … catch вызывается несколько методов, каждый из которых порождает свой вид исключений. Соответственно и «ловить» приходится несколько исключений.

Рассмотрим несложный пример — класс Generator в методе helloMessage порождает два исключения, а класс Starter будет ловить эти два исключения.

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

Ниже код для двух классов исключений.

Как видим ничего особенно сложного нет. В Java 1.7 появилась более компактная конструкция, в которой классы исключений перечисляются в одном catch через знак «|». Вот так:

catch (FirstException | SecondException ex) {
System.out.println(«Error message:» + ex.getMessage());
}

Но нередко все исключения можно обработать в одной ветке catch. В этом случае можно использовать полиморфизм и наследование исключений. Как и любой класс, исключение тоже наследуется. При построении секции catch это можно использовать. Принцип следующий — поиск подходящего исключения начинается с первого catch. Вы понимаете, что все наследники класса Exception подходят под этот класс. Значит если у нас самый первый catch будет ловить класс Exception, то туда будут попадать практически все исключения.
Давайте рассмотрим вот такой пример обработки:

Этот пример не будет компилироваться, т.к. второй и третий catch не будет достигнут никогда — мы все время попадаем в первую ветку catch. Но если мы переместим обработку Exception в конец, то этот код будет корректным, т.к. сначала мы проверяем более точные классы исключений. Код ниже на первый взгляд, не сильно отличается. но его можно скомпилировать.

Этим приемом нередко пользуется — учитесь строить, читать и распознавать такие конструкции.

Класс RuntimeException

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

Пожелания

Самое главное пожелание — не оставляйте блоки catch без информации. Очень неплохим подспорьем будет вызов метода printStackTrace. Очень важно понимать. что случилось, почему. И если блок catch не будет информативным, то выяснение причин будет крайне сложным занятием.
Ужасным кодом будет нечто вроде этого:

Что здесь можно понять в случае возникновения исключения ? НИЧЕГО ? Уважайте свой труд — выводите подробную информацию.

Что дальше ?

Рассмотрев исключения, мы завершили знакомство с конструкциями языка Java. Я говорю именно о конструкциях — у нас еще впереди долгий путь изучения. Но если выражаться образно, то алфавит освоили. Мы еще будем встречать некоторые конструкции и слова, но подавляющая часть уже пройдена. Впереди нас ждет изучение уже готовых классов, которые предназначены для решения различных задач.
Все конструкции служат главному — позволить вам создавать описания классов, создавать объекты и управлять последовательностью вызовов для решения своей задачи. Можно по-разному оценивать удобство, но раз это так сделано разработчиками Java — надо уметь этим пользоваться.
И еще раз выскажу пожелание — учитесь читать код, учитесь понимать программу, просто просматривая что она делает, шаг за шагом.

11 comments to Исключения

  • SeveneduS  says:

    Для начала хочу поблагодарить за стать.
    Пожалуйста переправьте:
    приер такого -> приМер такого описания.

  • Grif  says:

    «finally {
    // Этот блок будет вызываться всегда, независиОМ от результата
    System.out.println(«Этот блок вызываетя всегда»);

    Прошу прощения за досаду, маленькая опечатка, «О» и «М» надо местами поменять.

  • Я  says:

    Да что вы все заладили с поправками!
    Будет у автора возможность, прогонит он статьи через спеллчекер или поставит orphus и, если надо, стилистику текста поправит!
    Комментарии к статье не для того предназначены!
    Давайте еще спеллчекингом комментариев друг-друга займемся?! Это же интереснее, чем Java!

    • admin  says:

      На самом деле это удобно и я очень благодарен таким замечаниям — исправил достаточно много ошибок. Заниматься поиском и исправлением все никак не получается. Я со временем такие комментарии удаляю.

  • Aiven  says:

    Опять же поправка — «Я уже дважды использовать термин «бросать»»

    Спасибо за Ваш сайт, здесь есть всё что необходимо, кроме заданий для закрепления материала.

    • admin  says:

      Спасибо, исправил. По поводу заданий — это достаточно важный элемент, но просто задания давать не получится — их же еще надо проверить. Для этого надо что-то придумать — просто опубликовать ответы нельзя — будут подсматривать и эффект будет нулевой. Я в процессе поиска решения.

  • Serga  says:

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

    • admin  says:

      Не очень понял данный комментарий. Что значит «два последних кода совпадают» ?
      Если имеются в виду два куска в самом конце, которые включают обработку нескольких классов исключений, то они РАЗНЫЕ. Надо внимательно посмотреть.

      • Serga  says:

        извиняюсь, не заметил. отличные курсы 🙂

  • Дмитрий  says:

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

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

    • admin  says:

      Переопределяя метод, можно либо использовать те же исключения, либо можно сузить класс исключения — например заменить IOException на FileNotFoundException.
      Либо можно совсем убрать исключения у наследника. Вот и комбинируйте 🙂

Leave a reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Лимит времени истёк. Пожалуйста, перезагрузите CAPTCHA.