Свои исключения
Готовых исключений много, но они говорят о технических деталях: «деление на ноль», «нет файла». Ошибки предметной области — «недостаточно средств», «товар закончился» — удобнее описывать своими классами. Тогда по типу исключения сразу понятно, что случилось, и такие ошибки легко перехватывать отдельно.
Простейшее исключение
Своё исключение — это обычный класс, унаследованный от Exception или RuntimeException. Достаточно конструктора, который передаёт сообщение родителю через super:
class InsufficientFundsException extends Exception {
InsufficientFundsException(String message) {
super(message);
}
}
void main() {
try {
throw new InsufficientFundsException("На счёте всего $40");
} catch (InsufficientFundsException e) {
IO.println("Ошибка: " + e.getMessage());
}
}Результат:
Ошибка: На счёте всего $40Имя класса принято заканчивать словом Exception.
Exception или RuntimeException
От выбора родителя зависит, будет ли исключение проверяемым:
extends Exception— проверяемое. Компилятор заставит вызывающий код обработать его или объявитьthrows. Подходит для ожидаемых ситуаций, на которые надо реагировать: нехватка денег, занятое имя пользователя.extends RuntimeException— непроверяемое. Подходит для ошибок программиста и нарушений правил, которые проверять на каждом шаге неудобно.
В современном коде чаще выбирают RuntimeException: так меньше шаблонного кода. Но если ошибку действительно нужно обработать, проверяемое исключение не даст о ней забыть.
Исключение в методе
Метод, выбрасывающий проверяемое исключение, объявляет его в throws:
class InsufficientFundsException extends Exception {
InsufficientFundsException(String message) {
super(message);
}
}
class Account {
private int balance = 100;
void withdraw(int amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Нужно $" + amount + ", доступно $" + balance);
}
balance -= amount;
IO.println("Снято $" + amount + ", осталось $" + balance);
}
}
void main() {
Account account = new Account();
try {
account.withdraw(30);
account.withdraw(500);
} catch (InsufficientFundsException e) {
IO.println("Отказ: " + e.getMessage());
}
}Результат:
Снято $30, осталось $70
Отказ: Нужно $500, доступно $70Дополнительные поля
Исключение — обычный класс, поэтому в нём можно хранить данные об ошибке. Обработчику не придётся разбирать текст сообщения:
class OutOfStockException extends RuntimeException {
private final String product;
OutOfStockException(String product) {
super("Товар закончился: " + product);
this.product = product;
}
String getProduct() {
return product;
}
}
void main() {
try {
throw new OutOfStockException("Кофе");
} catch (OutOfStockException e) {
IO.println(e.getMessage());
IO.println("Предложим замену для: " + e.getProduct());
}
}Результат:
Товар закончился: Кофе
Предложим замену для: КофеПричина исключения
Часто своё исключение выбрасывают в ответ на чужое — например, на NumberFormatException при разборе настроек. Исходное исключение передают вторым аргументом конструктора, и оно становится причиной (cause):
class ConfigException extends RuntimeException {
ConfigException(String message, Throwable cause) {
super(message, cause);
}
}
int readPort(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new ConfigException("Неверный порт: " + value, e);
}
}
void main() {
try {
readPort("80a");
} catch (ConfigException e) {
IO.println(e.getMessage());
IO.println("Причина: " + e.getCause().getMessage());
}
}Результат:
Неверный порт: 80a
Причина: For input string: "80a"Частая ошибка
Если не передать причину, исходная ошибка теряется: в трассировке стека останется только ваше исключение, и понять, что на самом деле сломалось, будет трудно.
class ConfigException extends RuntimeException {
ConfigException(String message) {
super(message);
}
}
void main() {
try {
Integer.parseInt("80a");
} catch (NumberFormatException e) {
throw new ConfigException("Неверный порт");
}
}Результат:
Exception in thread "main" Main$ConfigException: Неверный порт
at Main.main(Main.java:11)Имя Main$ConfigException означает, что класс объявлен внутри файла с main: Java помещает такие классы внутрь неявного класса Main. Важнее другое — здесь не видно ни исходного значения, ни NumberFormatException. Добавьте в класс конструктор с параметром Throwable cause и передавайте e — тогда трассировка покажет блок Caused by: с исходной ошибкой.