Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Свои исключения

Готовых исключений много, но они говорят о технических деталях: «деление на ноль», «нет файла». Ошибки предметной области — «недостаточно средств», «товар закончился» — удобнее описывать своими классами. Тогда по типу исключения сразу понятно, что случилось, и такие ошибки легко перехватывать отдельно.

Простейшее исключение

Своё исключение — это обычный класс, унаследованный от 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: с исходной ошибкой.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.