Java throw и throws
Как вручную генерировать исключения с помощью throw и объявлять их в сигнатуре метода с помощью throws в Java.
До сих пор мы перехватывали исключения, которые генерировал чужой код. Теперь вы узнаете, как генерировать их самостоятельно. Два ключевых слова выполняют основную работу — и их легко перепутать, ведь они отличаются лишь одной буквой.
throw— оператор, который вызывает исключение во время выполнения. Одно слово, в коде, который исполняется.throws— объявление в сигнатуре метода, означающее «этот метод может генерировать исключения следующих типов». Проверяется компилятором и никогда не исполняется.
throw происходит. throws предупреждает. Запомните эту пару.
Генерация исключения
throw принимает выражение типа Throwable (или любого подтипа) и генерирует его. Текущий метод немедленно завершается, стек начинает раскручиваться, и исключение начинает поиск соответствующего блока catch.
if (amount < 0) {
throw new IllegalArgumentException("amount must be non-negative, got " + amount);
}Три важных момента:
- Можно генерировать только
Throwable. Компилятор это обеспечивает —throw "oops";не скомпилируется. - Всегда генерируется экземпляр, а не класс.
throw new X(...), но неthrow X. - Экземпляр может быть создан непосредственно при вызове (распространённый вариант) или быть уже существующим объектом (редкий вариант — исключения хранят трассировку стека с момента создания, поэтому повторное использование объекта «замораживает» неверную трассировку).
Когда генерировать исключение
Генерируйте исключение, когда текущий метод не может выполнить своё предназначение. Явные случаи:
- Недопустимые аргументы —
IllegalArgumentExceptionдля ситуации «вы вызвали меня неправильно». - Неверное состояние —
IllegalStateExceptionдля ситуации «вы вызвали меня в неподходящий момент» (например,next()на пустом итераторе). - Отсутствующие данные — доменно-специфичные исключения, такие как
UserNotFoundException. - Ошибки внешних операций — ошибки ввода-вывода, сетевые ошибки. Как правило, они приходят от только что вызванного метода, поэтому вы не создаёте их самостоятельно; вы либо позволяете им распространяться, либо оборачиваете в исключение более высокого уровня.
Случай, когда не стоит генерировать исключение: использование в качестве сокращения для управления потоком при нормальных результатах. «Генерация ради управления потоком» — медленная и запутывающая практика. Если «не найдено» является штатным результатом, возвращайте Optional<T>, а не NotFoundException.
Выбор типа
Встроенные исключения из java.lang охватывают большинство случаев без лишних усилий:
IllegalArgumentException— неверный аргументIllegalStateException— неверное состояниеNullPointerException— обязательный аргумент оказался null (используйтеObjects.requireNonNull)UnsupportedOperationException— операция не реализована (например,addу неизменяемого списка)ArithmeticException— ошибка в вычислениях
Когда сбой специфичен для вашего домена — «пользователь не найден», «недействительный купон», «конфигурация рассинхронизирована» — создайте небольшой пользовательский класс для него. Через две главы мы сделаем именно это.
Конструкция throws
Если ваш метод может генерировать проверяемое исключение, которое он сам не перехватывает, вы обязаны объявить его:
public Config loadConfig(Path p) throws IOException, ParseException {
String text = Files.readString(p);
return parser.parse(text);
}Конструкция является частью контракта метода. Она сообщает каждому вызывающему: «если вы вызываете меня, вы должны либо перехватить эти исключения, либо объявить их у себя». Компилятор это обеспечивает — именно это делает их проверяемыми.
Несколько правил:
- Объявляются только проверяемые исключения.
RuntimeExceptionи его подклассы являются непроверяемыми — их объявление допустимо, но не обязательно и обычно не практикуется. - Можно объявить больше типов, чем реально генерируется, — это полезно, когда вы оставляете место для будущих реализаций, хотя и создаёт небольшой «шум».
- Метод, переопределяющий другой, может объявлять те же или меньше проверяемых исключений, чем родительский (и только подтипы объявленных). Добавлять новые нельзя. Это применение принципа подстановки Лисков к исключениям.
throw и throws вместе
Реальный метод обычно использует оба:
public User loadUser(String id) throws IOException {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id must be non-blank");
}
String json = httpClient.get("/users/" + id); // may throw IOException
return parser.toUser(json);
}throws IOExceptionобъявляет проверяемое исключение, которое может возникнуть вhttpClient.get.throw new IllegalArgumentException(...)генерирует непроверяемое исключение при некорректных входных данных. Его не нужно указывать в конструкцииthrows.
Оборачивание исключения
Когда низкоуровневое исключение не несёт смысла на вашем уровне абстракции, оберните его в то, которое несёт. Передайте оригинал в качестве причины, чтобы трассировка сохранилась:
try {
return Files.readString(configPath);
} catch (IOException e) {
throw new ConfigLoadException("could not load config from " + configPath, e);
}Паттерн конструктора с двумя аргументами — (message, cause) — является стандартным для Exception, IOException и всех встроенных исключений. Когда вы пишете собственный класс исключения, предусмотрите оба конструктора.
Развёрнутый пример
Небольшой вспомогательный класс в стиле банковского приложения: он проверяет входные данные с помощью IllegalArgumentException, сигнализирует о пустом счёте с помощью IllegalStateException и позволяет проверяемому исключению распространяться к вызывающему через throws. Метод-драйвер показывает, как выглядит каждый из случаев при генерации.
Три случая во время выполнения — некорректный аргумент, неверное состояние и успешное снятие средств — попадают в один блок catch. Четвёртый, archive(), компилируется лишь потому, что main разрешено перехватывать Exception и потому что archive() объявил throws ArchiveException. Попробуйте убрать конструкцию throws — программа перестанет компилироваться.
Что дальше
Компилятор относится к одним исключениям строго (их необходимо обработать), а к другим — мягко (не обязательно). Это различие рассмотрено в следующей главе. Продолжайте с проверяемые и непроверяемые исключения в Java.