Перейти к содержимому
Теория · Исключения · урок 8 из 12

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

В уроке о проверяемых исключениях метод load впервые бросил исключение сам, оператором throw. В здании это кнопка пожарной тревоги на стене: нажать её может любой, кто заметил дым, не дожидаясь датчика. Разберём, когда нажимать её самому и как сделать тревогу со своей надписью.

Пусть метод withdraw снимает деньги со счёта. Что ему делать, если просят больше, чем есть? Можно вернуть -1, но вызывающий может забыть это проверить и напечатать -1 как остаток. Исключение забыть нельзя: если на него никто не ответит, программа остановится.

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

Для случая «не хватает денег» в Java нет готового класса, но его можно написать. Исключение — обычный класс, наследник Exception:

JAVA
public class Main {
    public static void main(String[] args) {
        var error = new InsufficientFundsException("need 50 more");
        System.out.println(error.getMessage()); // need 50 more
    }
}

class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message) {
        super(message);
    }
}

Давайте разберём по частям:

  1. class InsufficientFundsException extends Exception — новый класс исключения. Его родитель Exception, значит, исключение проверяемое. Имя по традиции кончается на Exception.
  2. Конструктор принимает сообщение и передаёт его родителю через super(message).
  3. Метод getMessage() объявлен в классе Throwable, и наш класс получает его по наследству. Он возвращает сообщение, которое передали в конструктор.
  4. В main объект создаётся через new, как любой другой. Ничего не случается: пока исключение не бросили, это обычный объект.

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

Бросить своё исключение

Теперь withdraw может нажать кнопку тревоги сам:

JAVA
static int withdraw(int balance, int amount)
        throws InsufficientFundsException {
    if (amount <= 0) {
        throw new IllegalArgumentException("amount must be positive");
    }
    if (amount > balance) {
        int lack = amount - balance;
        throw new InsufficientFundsException("need " + lack + " more");
    }
    return balance - amount;
}

Метод проверяет две разные беды:

  1. Сумма не больше нуля — это ошибка в коде того, кто вызвал метод. Для неё в Java есть готовый непроверяемый класс IllegalArgumentException, «неверный аргумент». NumberFormatException, который бросает parseInt, — его наследник.
  2. Денег не хватает — это обычная жизненная ситуация, и вызывающий должен быть к ней готов. О ней сообщает наше проверяемое исключение, поэтому в заголовке стоит throws InsufficientFundsException.

throw завершает метод сразу, как исключение от JVM: до return дело не доходит. Вызов withdraw теперь пишут в try:

JAVA
try {
    System.out.println(withdraw(100, 30)); // 70
    System.out.println(withdraw(100, 150));
} catch (InsufficientFundsException e) {
    System.out.println("Refused: " + e.getMessage()); // Refused: need 50 more
}

Разберём по строкам:

  1. withdraw(100, 30) возвращает 70, и программа его печатает.
  2. withdraw(100, 150) бросает исключение с сообщением need 50 more, и второй println не выполняется.
  3. catch ловит его и печатает Refused: need 50 more.

А вызов withdraw(100, -5) в том же try бросил бы IllegalArgumentException. Этот catch его не поймает, потому что класс другой, и программа остановится:

TEXT
Exception in thread "main" java.lang.IllegalArgumentException: amount must be positive

Так и задумано: ошибку в коде не прячут, а исправляют.

throw и throws

Эти два слова путают постоянно, и их различие часто спрашивают на собеседовании:

СловоГде стоитЧто делает
throwв теле методабросает один объект исключения сейчас
throwsв заголовке метода, после скобокобъявляет, какие исключения могут выйти из метода

throw — действие: нажать кнопку тревоги. throws — табличка на этаже «передаём наверх». Для проверяемого исключения табличка обязательна, для непроверяемого — нет.

Здесь картинка с кнопкой неточна. Настоящая сирена звучит одинаково, кто бы её ни включил. А исключение несёт свой класс и сообщение, и по классу catch решает, его это тревога или нет.

Частая ошибка

В своём классе исключения забыли передать сообщение родителю:

JAVA
class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message) { // ✗
    }
}

Программа компилируется, но вместо Refused: need 50 more печатает Refused: null. Конструктор не вызвал super(message), поэтому компилятор вставил super() без аргументов. Родитель так и не узнал сообщение, и getMessage() возвращает null. Передайте сообщение родителю: первой строкой конструктора напишите super(message);.

Главное

  • throw new … бросает исключение сразу и завершает метод; для неверного аргумента есть готовый IllegalArgumentException.
  • Своё исключение — класс-наследник Exception (проверяемое) или RuntimeException (непроверяемое), который передаёт сообщение через super(message).
  • getMessage() возвращает сообщение, переданное в конструктор.
  • throw бросает исключение в теле метода, а throws объявляет его в заголовке.