Что лучше: наследоваться от Exception или RuntimeException?
В Java есть два основных типа исключений:
🟢 Junior Level
Ключевой вопрос: должен ли вызывающий быть обязан обработать эту ошибку?
Decision Tree:
- Ошибка ввода/валидации →
IllegalArgumentException(unchecked) - Ошибка бизнес-правила →
BusinessException(unchecked) - Ошибка внешней системы → зависит:
- Если восстановимо (retry поможет) → checked
- Если нет (сервис упал) → unchecked
В Java есть два основных типа исключений:
- Checked Exception (наследуются от
Exception, но не отRuntimeException) — компилятор требует их обработки. Зачем: чтобы разработчик на этапе написания кода осознанно решил, что делать с этой ошибкой. Это форма документации, встроенная в язык. - Unchecked Exception (наследуются от
RuntimeException) — компилятор не требует их обработки. Почему: JVM спроектирована так, что эти исключения представляют ошибки программирования (баги), которые нельзя восстановить на лету — их нужно исправлять в коде, а не ловить.
// Checked — нужно объявлять throws или ловить
public class DatabaseException extends Exception {
public DatabaseException(String msg) { super(msg); }
}
// Unchecked — не нужно объявлять
public class ValidationException extends RuntimeException {
public ValidationException(String msg) { super(msg); }
}
Рекомендация (не абсолютное правило):
- Ошибки пользователя (неверный ввод, нарушение бизнес-правил) → обычно
RuntimeException, потому что вызывающий код не может их восстановить — он должен исправить логику - Ошибки внешней системы (сеть, БД, файловая система) → часто
Exception, потому что вызывающий код может предпринять действия: повторить запрос, переключиться на резервный сервер, уведомить пользователя
Пример использования
// Checked — внешняя система недоступна
public User findUser(Long id) throws DatabaseException {
// SQL может упасть — вызывающий код ДОЛЖЕН это знать
// В классическом JDBC -- checked (SQLException). В Spring Data/JPA -- unchecked (DataAccessException). Выбор зависит от фреймворка.
return userRepository.findById(id)
.orElseThrow(() -> new DatabaseException("User not found: " + id));
}
// Unchecked — ошибка программирования
public void validateAge(int age) {
if (age < 0) {
throw new ValidationException("Age cannot be negative");
}
}
🟡 Middle Level
Когда наследоваться от Exception (Checked)
Используй checked exception, когда:
- Вызывающий может восстановить ситуацию
public Connection getConnection() throws ConnectionPoolExhaustedException { // Вызывающий может подождать и попробовать снова } - Это ожидаемая ситуация, а не баг
public Document readDocument(String path) throws FileNotFoundException { // Файл может не существовать — это нормально } - API требует обязательной обработки
public void transferMoney(Account from, Account to, BigDecimal amount) throws InsufficientFundsException { // Вызывающий ОБЯЗАН обработать случай недостаточных средств }
Когда наследоваться от RuntimeException (Unchecked)
Используй unchecked exception, когда:
- Ошибка программирования (баг, неправильные аргументы)
public User getUser(Long id) { if (id == null) { throw new IllegalArgumentException("ID cannot be null"); } // ... } - Восстановление невозможно
public Config loadConfig(String path) { if (!Files.exists(path)) { throw new ConfigurationException("Config file missing: " + path); // Программа не может работать без конфигурации } } - Spring / современный стиль — большинство фреймворков предпочитает unchecked
@Service public class OrderService { public Order createOrder(OrderRequest req) { if (req.items().isEmpty()) { throw new BusinessRuleException("Order must have items"); } // Spring сам обработает RuntimeException → 400 Bad Request } }
Сравнительная таблица
| Критерий | Exception (Checked) | RuntimeException (Unchecked) |
|---|---|---|
| Обязательная обработка | Да | Нет |
| Объявление в throws | Обязательно | Не требуется |
| Типичное применение | Внешние системы, I/O | Ошибки логики, валидация |
| Восстановление | Возможно | Обычно невозможно |
| Spring обработка | Нужно @ControllerAdvice |
Автоматически → 400/500 |
| Verbosity кода | Высокая (try/catch повсюду) | Низкая |
Типичные ошибки
- Checked exception для валидации: ```java // ❌ Плохо — валидация не требует восстановления, вызывающий код не может исправить невалидный email на лету // Кроме того, checked exception заставляет писать try/catch повсюду, что затрудняет чтение кода public void setEmail(String email) throws InvalidEmailException { … }
// ✅ Хорошо — RuntimeException, потому что это ошибка программирования (передали неверный email) public void setEmail(String email) { if (!isValid(email)) { throw new InvalidEmailException(email); // RuntimeException } }
2. **Обёртывание checked в unchecked без причины:**
```java
// ❌ Теряется информация о реальной проблеме
try {
fileInputStream.read();
} catch (IOException e) {
throw new RuntimeException(e); // Lose context!
}
// ✅ Оборачиваем с сохранением причины и контекста
try {
fileInputStream.read();
} catch (IOException e) {
throw new DataReadException("Failed to read file: " + filePath, e);
}
Когда НЕ использовать RuntimeException
Не используйте RuntimeException (т.е. выбирайте checked Exception), если:
- Вы пишете публичную библиотеку — checked exception служит документацией: разработчик сразу видит, какие ошибки возможны, и обязан их обработать
- Вызывающий код может восстановить ситуацию — например, временная недоступность сервиса (retry поможет), или файл заблокирован другим процессом (подождать и повторить)
- Критическая система: игнорирование ошибки = финансовые потери или угроза безопасности. Checked exception — страховка от молчаливого провала
- Проект без единого глобального обработчика ошибок — unchecked exception пройдёт незамеченным, checked заставит хотя бы задуматься об обработке
Когда НЕ использовать checked Exception
Не используйте checked Exception (т.е. выбирайте RuntimeException), если:
- Это ошибка программирования —
nullвместо объекта, отрицательный возраст, пустой обязательный список. Такие ошибки нужно исправлять в коде, а не ловить - Вы используете Spring/Web-фреймворк — фреймворк уже имеет глобальный обработчик (
@ControllerAdvice), который превратит RuntimeException в правильный HTTP-статус - Слишком много boilerplate — если
throwsпроходит через 10 слоёв приложения, это шум, а не полезная информация
🔴 Senior Level
Архитектурная перспектива
Checked exceptions были экспериментом Java, который не прижился в других языках:
- C# — только unchecked
- Kotlin — только unchecked
- Go — multiple return values (error, result)
- Rust —
Result<T, E>(явная обработка на уровне типов)
Проблема checked exceptions: утечка абстракции
// ❌ DAO слой "протекает" в Controller через SQLException
public interface UserRepository {
User findById(Long id) throws SQLException; // SQLException — деталь реализации!
}
// ✅ Обернуть в domain-исключение
public interface UserRepository {
User findById(Long id); // Чистый интерфейс
// Внутри: catch (SQLException e) → throw new RepositoryException(e)
}
Spring и checked exceptions
Spring конвертирует большинство checked exceptions в unchecked:
@Repository
public class JpaUserRepository {
// JPA бросает PersistenceException (unchecked)
// Spring конвертирует в DataAccessException (unchecked)
// ControllerAdvice ловит → HTTP статус
}
Performance
В рантайме разницы нет – оба создают стек-трейс. Разница в разработке: checked требуют объявления throws во всех методах вверх по стеку, unchecked – нет. Это overhead разработки, не исполнения. Создание исключения (stack trace) – дорогая операция (~1-5 мкс), но тип наследования на это не влияет.
Modern Java Approach
// ✅ Domain Exceptions — unchecked, с контекстом
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
private final Long orderId;
public Long getOrderId() { return orderId; }
}
// ✅ Infrastructure Exceptions — unchecked, с причиной
public class ExternalServiceException extends RuntimeException {
public ExternalServiceException(String serviceName, Throwable cause) {
super("External service failed: " + serviceName, cause);
this.serviceName = serviceName;
}
private final String serviceName;
}
// ✅ Retryable Exceptions — unchecked, с маркером
public class TransientException extends RuntimeException {
// Помечает исключения, которые можно повторить
}
Decision Framework
Вопросы для принятия решения:
│
├── Вызывающий может ВОССТАНОВИТЬСЯ?
│ ├── Да → Exception (checked)
│ └── Нет → RuntimeException (unchecked)
│
├── Это ошибка ПРОГРАММИРОВАНИЯ?
│ ├── Да → RuntimeException (IllegalArgumentException, IllegalStateException)
│ └── Нет → Смотрим дальше
│
├── Это API для сторонних разработчиков?
│ ├── Да → Exception (forced handling)
│ └── Нет → RuntimeException (less boilerplate)
│
└── Фреймворк обрабатывает исключения? (Spring)
├── Да → RuntimeException (framework handles)
└── Нет → Зависит от контекста
Best Practices
// ✅ Иерархия domain-исключений
BaseException (RuntimeException)
├── ValidationException
├── NotFoundException
├── BusinessRuleException
└── ExternalServiceException
// ✅ Иерархия infrastructure-исключений
InfrastructureException (RuntimeException)
├── DatabaseException
├── NetworkException
└── TimeoutException
// ✅ Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
return ResponseEntity.status(404).body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(ValidationException.class)
ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
return ResponseEntity.status(400).body(new ErrorResponse(e.getMessage()));
}
}
🎯 Шпаргалка для интервью
Обязательно знать:
Exception(checked) — компилятор требует обработки;RuntimeException(unchecked) — не требует- Выбирайте checked, когда вызывающий может восстановиться (retry, fallback); unchecked — для ошибок программирования
- В Spring/Web-фреймворках предпочитайте unchecked —
@ControllerAdviceавтоматически маппит на HTTP-статусы - Checked exceptions — эксперимент Java, не прижился в C#, Kotlin, Go, Rust
- Оборачивайте checked в unchecked с сохранением причины (
new DataReadException("msg", e)) - Decision Framework: может восстановиться → checked; ошибка баг → unchecked; публичная библиотека → checked
Частые уточняющие вопросы:
- Почему Spring предпочитает unchecked? — Фреймворк уже имеет глобальный обработчик; checked добавляют boilerplate без пользы
- Что такое утечка абстракции через checked exceptions? — Когда
SQLException«протекает» из DAO в Controller черезthrows— деталь реализации попадает в интерфейс - Когда checked exception лучше? — Публичные библиотеки, критические системы (финансы, безопасность), временные сбои (retry поможет)
- Есть ли разница в производительности? — В рантайме нет, оба создают стек-трейс; разница в overhead разработки (throws через все слои)
Красные флаги (НЕ говорить):
- “Всегда использую checked — так безопаснее” — Это ведёт к boilerplate и утечке абстракций
- “Оборачиваю все checked в RuntimeException без причины” — Теряется информация о реальной проблеме
- “Checked exception = медленнее” — Разницы в рантайме нет, разница только в количестве кода
Связанные темы:
- [[13. Можно ли создавать кастомные исключения]]
- [[14. Когда стоит создавать свои исключения]]
- [[2. Что такое checked exception и когда его использовать]]
- [[3. Что такое unchecked exception (Runtime Exception)]]
- [[18. Что такое оборачивание (wrapping) исключений]]