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

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

Все исключения делятся на 4 вида, которые на самом деле являются классами, унаследованными друг от друга.

Класс Throwable

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

В оператор throw можно передать только объект класса-наследника Throwable. И хотя теоретическим можно написать код вида throw new Throwable(); — так обычно никто не делает. Главная цель существования класса Throwable — единый класс-предок для всех исключений.

Класс Error

Следующим классом исключений является класс Error — прямой наследник класса Throwable. Объекты типа Error (и его классов-наследников) создает Java-машина в случае каких-то серьезных проблем. Например, сбой в работе, нехватка памяти, и т.д.

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

Класс Exception

Исключения типа ExceptionRuntimeException) — это обычные ошибки, которые возникают во время работы многих методов. Цель каждого выброшенного исключения — быть захваченным тем блоком catch, который знает, что нужно сделать в этой ситуации.

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

Другими словами, если какая-то переменная оказалась равна null, метод выкинет NullPointerException, если в метод передали неверные аргументы — выкинет InvalidArgumentException, если в методе случайно было деление на ноль — ArithmeticException.

Класс RuntimeException

RuntimeException — это разновидность (подмножество) исключений Exception. Можно даже сказать, что RuntimeException — это облегченная версия обычных исключений (Exception): на такие исключения налагается меньше требований и ограничений

Об отличии Exception и RuntimeException вы узнаете дальше


2. Проверяемые исключения: throws, checked exceptions

Проверяемые исключения: throws, checked exceptions

Все исключения в Java делятся на 2 категории — проверяемые (checked) и непроверяемые (unchecked).

Все исключения, унаследованные от классов RuntimeException и Error, считаются unchecked-исключениями, все остальные — checked-исключениями.

Важно!

Спустя 20 лет после введения проверяемых исключений, почти все Java-программисты считают это ошибкой. 95% всех исключений в популярных современных фреймворках — непроверяемые. Тот же язык C#, который чуть ли не скопировал Java подчистую, не стал добавлять checked-исключения.

В чем же основное отличие checked-исключений от unchecked?

К checked-исключениям есть дополнительные требования. Звучат они примерно так.

Требование 1

Если метод выбрасывает checked-исключение, он должен содержать тип этого исключения в своем заголовке (сигнатуре метода). Чтобы все методы, которые вызывают данный метод, знали о том, что в нем может возникнуть такое «важное исключение».

Указывать checked-исключения надо после параметров метода после ключевого слова throws (не путать со throw). Выглядит это примерно так:

тип метод (параметры) throws исключение

Пример:

checked-исключение unchecked-исключение
public void calculate(int n) throws Exception
{
   if (n == 0)
      throw new Exception("n равно нулю!");
}
public void calculate(n)
{
   if (n == 0)
      throw new RuntimeException("n равно нулю!");
}

В примере справа наш код выкидывает unchecked-исключение — никаких дополнительных действий не нужно. В примере слева метод выкидывает checked-исключение, поэтому в сигнатуру метода добавили ключевое слово throws и указали тип исключения.

Если метод планирует выкидывать несколько checked-исключений, все их нужно указать после ключевого слова throws через запятую. Порядок неважен. Пример:

public void calculate(int n) throws Exception, IOException
{
   if (n == 0)
      throw new Exception("n равно нулю!");
   if (n == 1)
      throw new IOException("n равно единице");
}

Требование 2

Если вы вызываете метод, у которого в сигнатуре прописаны checked-исключения, то вы не можете проигнорировать этот факт.

Вы должны либо перехватить все эти исключения, добавив блоки catch для каждого из них, либо добавить их в throws своего метода.

Мы как бы говорим себе: эти исключения настолько важные, что мы обязательно должны их перехватить. А если мы не знаем, как их перехватить, мы должны уведомить тех, кто будет вызывать наш метод, что в нем могут возникнуть такие исключения.

Пример:

Представим, что мы пишем метод, который должен создать мир, населенный людьми. Начальное количество человек передается в качестве параметра. Тогда мы должны добавить исключения, если людей слишком мало.

Создаем Землю Примечание
public void создатьМир(int n) throws ПустойМир,ОдинокийМир
{
   if (n == 0)
      throw new ПустойМир("Людей вообще нет!");
   if (n == 1)
      throw new ОдинокийМир("Слишком мало людей!");
   System.out.println("Создан прекрасный мир. Население: " + n);
}
Метод потенциально кидает два checked-исключения:

  • ПустойМир
  • ОдинокийМир

Вызов этого метода можно обработать 3 способами:

1. Не перехватываем возникающие исключения

Чаще всего это делается в случае, когда в методе не известно, как правильно обработать эту ситуацию.

Код Примечание
public void создатьНаселенныйМир(int population)
throws ПустойМир, ОдинокийМир
{
   создатьМир(population);
}
Вызывающий метод не перехватывает исключения и вынужден информировать о них других: добавить их себе в throws

2. Перехватывать часть исключений

Обрабатываем понятные ошибки, непонятные — прокидываем в вызывающий метод. Для этого нужно добавить их название в throws:

Код Примечание
public void создатьНепустойМир(int population)
throws ПустойМир
{
   try
   {
      создатьМир(population);
   }
   catch (ОдинокийМир e)
   {
      e.printStackTrace();
   }
}
Вызывающий метод перехватывает только одно checked-исключениеОдинокийМир, второе он должен добавить в свою сигнатуру: указать после слова throws

3. Перехватываем все исключения

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

Код Примечание
public void создатьЛюбойМир(int population)
{
   try
   {
      создатьМир(population);
   }
   catch(ОдинокийМир e)
   {
      e.printStackTrace();
   }
   catch(ПустойМир e)
   {
      e.printStackTrace();
   }
}
В этом методе перехватываются все ошибки. Вызывающий метод будет уверен, что все прошло отлично.


3. Обертывание исключений

Checked-исключения казались классной вещью в теории и оказались полным разочарованием на практике.

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

Поэтому вам придется добавить checked-исключение в throws всех методов, которые вызывают ваш суперпопулярный метод. А также в throws всех методов, которые вызывают те методы. И в методы, которые вызывает те методы.

В результате у вас в throws у половины методов проекта будет добавлено новое checked-исключение. А потом окажется, что у вас проект покрыт тестами, и тесты не компилируются. И вам придется править throws еще и в тестах.

А потом весь ваш код (изменения в сотнях файлов) должны будут ревьюить другие программисты. И тут мы задаем себе вопрос: а ради чего мы вносили в проект дохреналион изменений? День(дни?) работы, сломанные тесты, и все ради добавления одного checked-исключения?

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

Однако все еще много кода (в том числе и код стандартных библиотек Java) содержат эти самые checked-исключения. И что же с ними делать? Игнорировать нельзя, обрабатывать – неизвестно как.

Java-программисты предложили «заворачивать» checked-исключения внутрь RuntimeException. Другими словами, перехватывать все checked-исключения, создавать вместо них unchecked-исключения (например, RuntimeException) и выбрасывать уже их. Выглядит это все примерно так:

try
{
   код, где может возникнуть checked-исключение
}
catch(Exception exp)
{
   throw new RuntimeException(exp);
}

Не сильно красивое решение, но ничего криминального: исключение просто положили внутрь исключения RuntimeException.

При желании его можно оттуда легко достать. Пример:

Код Примечание
try
{
   // код где мы запаковали checked исключение
   // в RuntimeException
}
catch(RuntimeException e)
{
   Throwable cause = e.getCause();
   if (cause instanceof Exception)
   {
      Exception exp = (Exception) cause;
      // тут код по обработке Exception
   }
}







Получаем исключение, сохраненное внутри объекта RuntimeException. cause может быть null

Определяем его тип и преобразовываем к переменной checked-типа.


4. Множественный перехват исключений

Программисты очень не любят дублирование кода. Даже придумали такой принцип разработки — DRY: Don’t Repeat Yourself. Однако при обработке исключений часто возникают ситуации, когда после блока try следует несколько блоков catch с одинаковым кодом.

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

Начиная с 7-й версии, в язык Java добавили возможность указать несколько типов исключений в одном блоке catch. Выглядит это примерно так:

try
{
   код, где может возникнуть ошибка
}
catch(ТипИсключения1 | ТипИсключения2 | ТипИсключения3 имя)
{
   код обработки исключений
}

Блоков catch может быть сколько угодно. Однако в одном блоке catch нельзя указать исключения, которые наследуются друг от друга. Т.е. нельзя написать catch (Exception | RuntimeException e), т.к. класс RuntimeException унаследован от Exception.



5. Собственные исключения

Вы всегда можете создать свой собственный класс-исключение. Просто унаследовав класс от, например, RuntimeException. Выглядеть это будет примерно так:

class ИмяКласса extends RuntimeException
{
}

Детали мы обсудим, когда вы изучите ООП, наследование, конструкторы и переопределение методов.

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

Код Примечание
class Solution
{
   public static void main(String[] args)
   {
      throw new MyException();
   }
}

class MyException extends RuntimeException
{
}




Выбрасываем unchecked-исключение MyException.

Детальную работу со своими исключениями мы рассмотрим в квесте Java Multithreading.