Создание собственного класса исключения в Java
Класс исключения скомпилировался, выбрасывается и перехватывается — а e.getMessage() всё равно печатает null. Так бывает, если сохранить текст ошибки в собственном поле и забыть вызвать super(message):
public class ValidationException extends Exception {
private String message;
public ValidationException(String message) {
this.message = message; // super(message) не вызван
}
}
// где-то в коде:
try {
throw new ValidationException("Email не может быть пустым");
} catch (ValidationException e) {
System.out.println(e.getMessage()); // Вывод: null
} Собственное (пользовательское, custom) исключение в Java — это класс, унаследованный от Exception (проверяемое исключение) или от RuntimeException (непроверяемое). Чтобы создать свое исключение, достаточно объявить класс с суффиксом Exception, выбрать родителя и добавить конструкторы, передающие сообщение и причину в super(...). Всё остальное — getMessage(), getCause(), printStackTrace() — класс получит по наследству.
Как создать свое исключение в Java
Чтобы создать класс исключения, объявите класс, который наследует Exception или RuntimeException, и добавьте конструктор, вызывающий super(message). Реализовывать какие-либо методы не нужно: минимальное собственное исключение состоит из объявления класса и одного конструктора.
public class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
} Такое исключение выбрасывают оператором throw, а поскольку оно наследует Exception, метод обязан объявить его в throws:
public class AgeCheck {
static void checkAge(int age) throws InvalidAgeException {
if (age < 0 || age > 150) {
throw new InvalidAgeException("Недопустимый возраст: " + age);
}
System.out.println("Возраст принят: " + age);
}
public static void main(String[] args) {
try {
checkAge(25);
checkAge(-3);
} catch (InvalidAgeException e) {
System.out.println("Ошибка: " + e.getMessage());
}
}
} Вывод:
Возраст принят: 25
Ошибка: Недопустимый возраст: -3 Откуда берутся методы
Сам класс Exception объявляет только конструкторы. Методы getMessage(), getCause(), getStackTrace(), printStackTrace() и toString() он наследует от Throwable — поэтому и ваш подкласс получает их без единой строки кода. Полный список методов — в уроке Иерархия исключений в Java.
extends Exception или extends RuntimeException
Наследуйте Exception, если вызывающий код может и должен обработать ситуацию (компилятор заставит его это сделать), и RuntimeException, если исключение сигнализирует об ошибке программиста или нарушении контракта метода. Родитель определяет, будет ли исключение проверяемым (checked) или непроверяемым (unchecked).
| Родительский класс | Тип исключения | Нужен ли throws / try-catch | Когда выбирать |
|---|---|---|---|
extends Exception | Проверяемое (checked) | Да, иначе ошибка компиляции unreported exception ... must be caught or declared to be thrown | Ситуация ожидаема и восстановима: платёж отклонён, файл не найден, неверный пароль |
extends RuntimeException | Непроверяемое (unchecked) | Нет | Нарушение контракта или ошибка в логике: некорректный аргумент, недопустимое состояние объекта |
extends Error | Непроверяемое | Нет | Не используйте: Error зарезервирован для проблем уровня JVM (OutOfMemoryError, StackOverflowError) |
extends Throwable | Проверяемое | Да | Не используйте: такое исключение не поймает привычный catch (Exception e) |
Подробно о том, какие классы относятся к проверяемым и непроверяемым, — в уроке Иерархия исключений в Java.
Практическое правило
Спросите себя: «Что разумного может сделать вызывающий код, поймав это исключение?» Если есть осмысленная реакция (повторить запрос, показать пользователю сообщение, выбрать другой путь) — делайте исключение проверяемым. Если единственный вариант — исправить код, выбирайте RuntimeException. Современные фреймворки (Spring, Hibernate) почти все свои исключения делают непроверяемыми.
Конструкторы исключения: message и cause
Конструкторы не наследуются, поэтому каждый нужный вариант надо объявить в своём классе и передать параметры в super(...). Обычно достаточно двух: с сообщением (String message) и с сообщением и причиной (String message, Throwable cause) — второй позволяет не потерять исходное исключение при обёртывании.
Класс Exception (как и RuntimeException) предоставляет пять конструкторов, которые можно вызвать через super:
| Конструктор | Что сохраняется | Когда использовать |
|---|---|---|
Exception() | Ничего: getMessage() вернёт null | Редко: когда сам тип исключения говорит всё |
Exception(String message) | Текст ошибки | Основной случай |
Exception(String message, Throwable cause) | Текст и исходное исключение | Обёртывание низкоуровневого исключения (SQL, IO, парсинг) в своё |
Exception(Throwable cause) | Причину; сообщение = cause.toString() | Когда к причине нечего добавить |
protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) | Всё перечисленное плюс флаги (Java 7+) | Особые случаи: исключение без stack trace или без suppressed-исключений |
Пример класса исключения с двумя конструкторами и цепочкой исключений (exception chaining): NumberFormatException оборачивается в собственное исключение, а исходная причина остаётся доступна через getCause().
public class OrderProcessingException extends Exception {
public OrderProcessingException(String message) {
super(message);
}
public OrderProcessingException(String message, Throwable cause) {
super(message, cause);
}
} public class QuantityParser {
static int parseQuantity(String input) throws OrderProcessingException {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new OrderProcessingException("Некорректное количество товара: " + input, e);
}
}
public static void main(String[] args) {
try {
parseQuantity("12a");
} catch (OrderProcessingException e) {
System.out.println(e.getMessage());
System.out.println("Причина: " + e.getCause());
}
}
} Вывод:
Некорректное количество товара: 12a
Причина: java.lang.NumberFormatException: For input string: "12a" Если бы в конструктор не передали e, вторая строка вывела бы Причина: null, а в stack trace пропал бы блок Caused by: java.lang.NumberFormatException — при отладке пришлось бы угадывать, что именно пошло не так.
Исключение с дополнительными полями и toString()
В собственное исключение можно добавить поля с данными об ошибке — код, некорректное значение, идентификатор — и геттеры к ним. Тогда обработчик получает не только текст, но и структурированную информацию. Метод toString() переопределяют, когда нужен свой формат вывода исключения в лог или консоль.
public class MyException extends Exception {
private final int detail;
public MyException(int detail, String message) {
super(message);
this.detail = detail;
}
public int getDetail() {
return detail;
}
@Override
public String toString() {
return "MyException{"
+ "detail=" + detail
+ ", message=" + getMessage()
+ "}";
}
} public class MyExceptionExample {
public static void main(String[] args) {
try {
compute(1);
compute(20);
} catch (MyException e) {
System.out.println("Перехвачено исключение: " + e);
}
}
public static void compute(int a) throws MyException {
System.out.println("Вызван метод compute(" + a + ")");
if (a > 10) {
throw new MyException(a, "Значение больше 10");
}
System.out.println("Нормальное завершение.");
}
} Вывод:
Вызван метод compute(1)
Нормальное завершение.
Вызван метод compute(20)
Перехвачено исключение: MyException{detail=20, message=Значение больше 10} При конкатенации "..." + e вызывается toString(). Без переопределения сработала бы реализация из Throwable, которая возвращает «полное имя класса: сообщение», и последняя строка выглядела бы так: Перехвачено исключение: MyException: Значение больше 10 (для класса в пакете — с именем пакета). Поле detail в неё не попало бы, но осталось бы доступно через e.getDetail().
Пример непроверяемого исключения: extends RuntimeException
Непроверяемое собственное исключение создаётся так же, только родителем указывается RuntimeException. Объявлять его в throws и оборачивать вызов в try-catch не обязательно: компилятор этого не проверяет, а перехватывать исключение можно там, где это действительно нужно.
public class InsufficientFundsException extends RuntimeException {
private final long shortage;
public InsufficientFundsException(long shortage) {
super("Недостаточно средств, не хватает: " + shortage);
this.shortage = shortage;
}
public long getShortage() {
return shortage;
}
} public class Account {
private long balance = 100;
public void withdraw(long amount) { // throws не нужен
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
public static void main(String[] args) {
Account account = new Account();
account.withdraw(30); // баланс 70
try {
account.withdraw(500);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.println("Сумма к пополнению: " + e.getShortage());
}
}
} Вывод:
Недостаточно средств, не хватает: 430
Сумма к пополнению: 430 Если убрать try-catch, программа скомпилируется, но завершится с Exception in thread "main" InsufficientFundsException: Недостаточно средств, не хватает: 430 и stack trace. С проверяемым исключением тот же код без try-catch или throws не скомпилировался бы.
Когда создавать свое исключение, а когда взять стандартное
Свое исключение стоит создавать, когда ошибка имеет смысл в предметной области (OrderNotFoundException, WrongPasswordException), когда нужно передать дополнительные данные или когда вызывающий код должен перехватывать именно эту ошибку отдельно от других. Для типовых нарушений контракта лучше взять стандартное исключение из JDK — его смысл понятен любому Java-разработчику.
| Ситуация | Стандартное исключение |
|---|---|
| Недопустимое значение аргумента метода | IllegalArgumentException |
Аргумент равен null, хотя не должен | NullPointerException (через Objects.requireNonNull(arg, "...")) |
| Метод вызван в неподходящем состоянии объекта (например, соединение уже закрыто) | IllegalStateException |
| Индекс вне допустимого диапазона | IndexOutOfBoundsException (через Objects.checkIndex(index, length), Java 9+) |
| Операция не поддерживается реализацией | UnsupportedOperationException |
Типичный сценарий для собственного исключения — граница слоёв приложения: слой доступа к данным перехватывает SQLException и выбрасывает своё DataAccessException с причиной, чтобы бизнес-логика не зависела от деталей JDBC. Если исключений много, их объединяют общим родителем (например, ShopException и его подклассы) — тогда можно перехватить всю группу одним catch. Помните, что catch подкласса должен стоять раньше catch родителя, иначе код не скомпилируется: подробнее — в разделе Несколько блоков catch и их порядок.
На чём чаще всего ошибаются
Большинство проблем с собственными исключениями связаны не с синтаксисом, а с потерей информации: пустое сообщение, потерянная причина, неудачно выбранный родитель. Компилятор такие ошибки не ловит — они проявляются уже при отладке.
- Своё поле
messageвместоsuper(message). Как в примере в начале урока:getMessage()и stack trace покажутnull. Исправление — удалить поле и вызватьsuper(message)в конструкторе. - Потеря причины.
throw new MyException("Ошибка БД")внутриcatch (SQLException e)теряет исходное исключение: его stack trace не попадёт в лог. Пишитеthrow new MyException("Ошибка БД", e)и объявляйте конструктор сThrowable cause. - Наследование от
ThrowableилиError. Первое не поймаетcatch (Exception e), второе вводит в заблуждение:Errorозначает сбой JVM, а не ошибку приложения. - Имя без суффикса
Exception. КлассWrongPasswordформально допустим, но по соглашению имена исключений заканчиваются наException— так их сразу видно в коде и логах. - Предупреждение о
serialVersionUID.ThrowableреализуетSerializable, поэтому IDE и компилятор с флагом-Xlint:serialпредупреждают об отсутствии поля. Это не ошибка; чтобы убрать предупреждение, добавьтеprivate static final long serialVersionUID = 1L;. - Разные правила
throwsпри переопределении. Переопределённый метод не может объявлять более широкие проверяемые исключения, чем метод родителя, — это касается и ваших классов исключений. Правила разобраны в уроке Исключения при наследовании.
Часто задаваемые вопросы
Можно ли сделать исключение generic-классом или record?
Нет. Класс-наследник Throwable не может иметь параметров типа: class MyException<T> extends Exception даёт ошибку компиляции a generic class may not extend java.lang.Throwable, потому что из-за стирания типов catch не смог бы различить MyException<String> и MyException<Integer>. Record тоже не подходит: он неявно наследует java.lang.Record и не может расширять другой класс.
Обязательно ли добавлять serialVersionUID в класс исключения?
Не обязательно: код компилируется и работает без него. Но Throwable реализует Serializable, и если исключения передаются между JVM (RMI, распределённые кэши) или сериализуются в файлы, явный serialVersionUID защищает от InvalidClassException после изменения класса. Заодно исчезает предупреждение IDE.
Как создать исключение без stack trace, чтобы оно работало быстрее?
Вызовите защищённый конструктор, появившийся в Java 7: super(message, null, false, false). Последний параметр writableStackTrace = false отключает заполнение стека, и создание исключения становится заметно дешевле. Платить за это приходится отладкой: в логе не будет видно, где исключение возникло, поэтому приём оправдан только для исключений, которые выбрасываются очень часто и всегда обрабатываются.
Можно ли объявить класс исключения внутри другого класса?
Да. Обычно это статический вложенный класс: public static class NotFoundException extends RuntimeException внутри сервиса, к которому оно относится; снаружи к нему обращаются как UserService.NotFoundException. Нестатический внутренний класс тоже допустим, если внешний класс не generic, но его экземпляр привязан к объекту внешнего класса, поэтому для исключений его почти не используют.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
Все задачи модуля →
Комментарии