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

Виды исключений

Как мы и говорили, классов-исключений в Java очень много, почти 400! Но все они делятся на группы, так что запомнить их довольно легко. Вот как это выглядит: Все исключение имеют общий класс-предок Throwable. Он него происходят две больших группы — исключения (Exception) и ошибки (Error). Error — это критическая ошибка во время исполнения программы, связанная с работой виртуальной машины Java. В большинстве случаев Error не нужно обрабатывать, поскольку они свидетельствуют о каких-то серьезных недоработках в коде. Наиболее известные ошибки — StackOverflowError (возникает, например, когда метод бесконечно вызывает сам себя) и OutOfMemoryError (возникает, когда недостаточно памяти для создания новых объектов). Как видишь, в этих ситуациях, чаще всего, обрабатывать особо нечего — код просто неправильно написан и его нужно переделывать. Exceptions — это, собственно, исключения; исключительная, незапланированная ситуация, которая произошла при работе программы. Это не такие серьезные ошибки, как Error, но тем не менее — они требуют нашего внимания. Все исключения делятся на 2 вида — проверяемые (checked) и непроверяемые (unchecked). Все проверяемые исключения происходят от класса Exception. Что значит “проверяемые”? Мы частично упоминали об этом в прошлой лекции: “...компилятор Java знает о самых распространенных исключениях, и знает в каких ситуациях они могут возникнуть”. Например, он знает, что если программист в коде считывает данные из файла — легко может возникнуть ситуация, что файл не существует. И таких ситуаций (которые он может заранее предположить) очень много. Поэтому компилятор заранее проверяет наш код на наличие таких потенциальных исключений. Если он их находит — то не скомпилирует код, пока мы не обработаем их или не пробросим наверх. Второй вид исключений — “непроверяемые”. Они происходят от класса RuntimeException. Чем же они отличаются от проверяемых? Казалось бы — тоже есть куча разных классов, которые происходят от RuntimeException и описывают конкретные runtime-исключения. Разница в том, что этих ошибок компилятор не ожидает. Он как бы говорит: “На момент написания кода я ничего подозрительного не обнаружил, но при его работе что-то пошло не так. Видимо, в коде есть ошибки!” И это действительно так. Непроверяемые исключения чаще всего являются следствием ошибок программиста. А компилятор явно не может предусмотреть все возможные неправильные ситуации, которые люди могут создать своими руками:) Поэтому он не будет проверять обработку таких исключений в нашем коде. Ты уже сталкивался с несколькими непроверяемыми исключениями:
  • ArithmeticException возникает при делении на ноль
  • ArrayIndexOutOfBoundsException возникает при попытке обратиться к ячейке за пределами массива.
Теоретически, конечно, создатели Java могли бы ввести обязательную обработку таких исключений, но во что бы тогда превратился код? При любой операции деления чисел пришлось бы писать try-catch для проверки - не на ноль ли ты случайно делишь? При любом обращении к массиву надо было бы писать try-catch чтобы проверить, не вышел ли ты за эти пределы. Любой написанный код представлял бы собой спагетти и был бы совершенно нечитаем. Логично, что от этой идеи отказались. Поэтому непроверяемые исключения нет необходимости обрабатывать в блоках try-catch или пробрасывать наверх (хотя технически это возможно, как и с Error).

Как выбросить свое исключение

Разумеется, создатели Java не в состоянии предусмотреть все исключительные ситуации, которые могут возникнуть в программах. Программ в мире слишком много и они слишком разные. Но это не страшно, поскольку при необходимости ты можешь создавать свои собственные исключения. Это делается очень легко. Тебе достаточно создать собственный класс (его название должно заканчиваться на “Exception”. Компилятору это не нужно, но читающим твой код программистам сразу будет понятно, что это класс-исключение). Кроме того, нужно указать что класс происходит от класса Exception (это уже нужно для компилятора) и корректной работы. Например, у нас есть класс Собака — Dog. Мы можем погулять с собакой с помощью метода walk(). Но перед этим нам нужно проверить — надеты ли на нашего питомца ошейник, поводок и намордник. Если что-то из этого отсутствует — выбросим наше собственное исключение DogIsNotReadyException. Его код будет выглядеть так:
public class DogIsNotReadyException extends Exception {

   public DogIsNotReadyException(String message) {
       super(message);
   }
}
Чтобы указать, что класс является исключением, нужно написать extends Exception после имени класса (это значит — “класс происходит от класса Exception”). В конструкторе мы просто вызовем конструктор класса Exception со строкой message (она будет отображать пользователю сообщение от системы с описанием ошибки при ее возникновении). Вот как это будет выглядеть в коде нашего класса:
public class Dog {

   String name;
   boolean isCollarPutOn;
   boolean isLeashPutOn;
   boolean isMuzzlePutOn;

   public Dog(String name) {
       this.name = name;
   }

   public static void main(String[] args) {

   }

   public void putCollar() {

       System.out.println("Ошейник надет!");
       this.isCollarPutOn = true;
   }

   public void putLeash() {

       System.out.println("Поводок надет!");
       this.isLeashPutOn = true;
   }

   public void putMuzzle() {
       System.out.println("Намордник надет!");
       this.isMuzzlePutOn = true;
   }

   public void walk() throws DogIsNotReadyException {

   System.out.println("Собираемся на прогулку!");
   if (isCollarPutOn && isLeashPutOn && isMuzzlePutOn) {
       System.out.println("Ура, идем гулять! " + name + " очень рад!");
   } else {
       throw new DogIsNotReadyException("Собака " + name + " не готова к прогулке! Проверьте экипировку!");
   }
 }

}
Теперь наш метод walk() выбрасывает исключение DogIsNotReadyException. Это делается с помощью ключевого слова throw. Как мы уже говорили ранее, исключение является объектом. Поэтому в нашем методе при возникновении исключительной ситуации (на собаке чего-то не хватает) мы создаем новый объект класса DogIsNotReadyException и выбрасываем в программу с помощью слова throw. К сигнатуре метода walk() мы прибавляем throws DogIsNotReadyException. Иными словами, теперь компилятор в курсе что вызов метода walk() может обернуться исключительной ситуацией. Поэтому, когда мы вызовем это где-то в программе — это исключение нужно будет обработать. Попробуем сделать это в методе main():
public static void main(String[] args) {

   Dog dog = new Dog("Мухтар");
   dog.putCollar();
   dog.putMuzzle();
   dog.walk();//Unhandled exception: DogIsNotReadyException
}
Не компилируется, исключение не обработано! Обернем наш код в блок try-catch для обработки исключения:
public static void main(String[] args) {

   Dog dog = new Dog("Мухтар");
   dog.putCollar();
   dog.putMuzzle();
   try {
       dog.walk();
   } catch (DogIsNotReadyException e) {
       System.out.println(e.getMessage());
       System.out.println("Проверяем снаряжение! Ошейник надет? " + dog.isCollarPutOn + "\r\n Поводок надет? "
       + dog.isLeashPutOn + "\r\n Намордник надет? " + dog.isMuzzlePutOn);
   }
}
Теперь посмотрим на вывод в консоль: Ошейник надет! Намордник надет! Собираемся на прогулку! Собака Мухтар не готова к прогулке! Проверьте экипировку! Проверяем снаряжение! Ошейник надет? true Поводок надет? false Намордник надет? true Посмотри, насколько более информативным стал вывод в консоль! Мы видим каждый шаг, который происходил в программе; видим, в каком месте возникла ошибка, и также сразу можем увидеть, чего именно не хватает нашей собаке:) Вот так и создаются собственные исключения. Как видишь — ничего сложного. И хотя разработчики Java не удосужились добавить в свой язык специальное исключение для неправильно экипированных собак — мы исправили их оплошность:)