1. Виды исключений
Все исключения делятся на 4 вида, которые на самом деле являются классами, унаследованными друг от друга.
Класс Throwable
Самым базовым классом для всех исключений является класс Throwable
. В классе Throwable
содержится код, который записывает текущий стек-трейс вызовов функций в массив. Что такое стек-трейс вызовов, мы изучим немного позднее.
В оператор throw можно передать только объект класса-наследника Throwable
. И хотя теоретически можно написать код вида throw new Throwable();
— так обычно никто не делает. Главная цель существования класса Throwable
— единый класс-предок для всех исключений.
Класс Error
Следующим классом исключений является класс Error
— прямой наследник класса Throwable
. Объекты типа Error
(и его классов-наследников) создает Java-машина в случае каких-то серьезных проблем. Например, сбой в работе, нехватка памяти, и т.д.
Обычно вы как программист ничего не можете сделать в ситуации, когда в программе возникла ошибка типа Error
: слишком серьезна такая ошибка. Все, что вы можете сделать — уведомить пользователя, что программа аварийно завершается или записать всю известную информацию об ошибке в лог программы.
Класс Exception
Исключения типа Exception
(и RuntimeException
) — это обычные ошибки, которые возникают во время работы многих методов. Цель каждого выброшенного исключения — быть захваченным тем блоком catch
, который знает, что нужно сделать в этой ситуации.
Когда какой-то метод не может выполнить свою работу по какой-то причине, он сразу должен уведомить об этом вызывающий метод, выбрасывая исключение соответствующего типа.
Другими словами, если какая-то переменная оказалась равна null
, метод выкинет NullPointerException
, если в метод передали неверные аргументы — выкинет InvalidArgumentException
, если в методе случайно было деление на ноль — ArithmeticException
.
Класс RuntimeException
RuntimeException
— это разновидность (подмножество) исключений Exception
. Можно даже сказать, что RuntimeException
— это облегченная версия обычных исключений (Exception
): на такие исключения налагается меньше требований и ограничений
Об отличии Exception
и RuntimeException
вы узнаете дальше
2. Проверяемые исключения: 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-исключение |
---|---|
|
|
В примере справа наш код выкидывает 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
своего метода.
Мы как бы говорим себе: эти исключения настолько важные, что мы обязательно должны их перехватить. А если мы не знаем, как их перехватить, мы должны уведомить тех, кто будет вызывать наш метод, что в нем могут возникнуть такие исключения.
Пример:
Представим, что мы пишем метод, который должен создать мир, населенный людьми. Начальное количество человек передается в качестве параметра. Тогда мы должны добавить исключения, если людей слишком мало.
Создаем Землю | Примечание |
---|---|
|
Метод потенциально кидает два checked-исключения:
|
Вызов этого метода можно обработать 3 способами:
1. Не перехватываем возникающие исключения
Чаще всего это делается в случае, когда в методе не известно, как правильно обработать эту ситуацию.
Код | Примечание |
---|---|
|
Вызывающий метод не перехватывает исключения и вынужден информировать о них других: добавить их себе в throws |
2. Перехватывать часть исключений
Обрабатываем понятные ошибки, непонятные — прокидываем в вызывающий метод. Для этого нужно добавить их название в throws:
Код | Примечание |
---|---|
|
Вызывающий метод перехватывает только одно checked-исключение – ОдинокийМир , второе он должен добавить в свою сигнатуру: указать после слова throws |
3. Перехватываем все исключения
Если метод не прокидывает исключения вызывающему методу, вызывающий метод всегда будет уверен, что все выполнилось хорошо. И не сможет предпринять никаких действий, чтобы исправить ситуацию.
Код | Примечание |
---|---|
|
В этом методе перехватываются все ошибки. Вызывающий метод будет уверен, что все прошло отлично. |
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
.
При желании его можно оттуда легко достать. Пример:
Код | Примечание |
---|---|
|
Получаем исключение, сохраненное внутри объекта 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
{
}
Детали мы обсудим, когда вы изучите ООП, наследование, конструкторы и переопределение методов.
Однако, даже если у вас есть только такой простой класс — вообще без кода, вы все равно можете выбрасывать его исключения:
Код | Примечание |
---|---|
|
Выбрасываем unchecked-исключение MyException . |
Детальную работу со своими исключениями мы рассмотрим в квесте Java Multithreading.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ