Вопрос 15 · Раздел 7

Что лучше: наследоваться от Exception или RuntimeException?

В Java есть два основных типа исключений:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Ключевой вопрос: должен ли вызывающий быть обязан обработать эту ошибку?

Decision Tree:

  1. Ошибка ввода/валидации → IllegalArgumentException (unchecked)
  2. Ошибка бизнес-правила → BusinessException (unchecked)
  3. Ошибка внешней системы → зависит:
    • Если восстановимо (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, когда:

  1. Вызывающий может восстановить ситуацию
    public Connection getConnection() throws ConnectionPoolExhaustedException {
     // Вызывающий может подождать и попробовать снова
    }
    
  2. Это ожидаемая ситуация, а не баг
    public Document readDocument(String path) throws FileNotFoundException {
     // Файл может не существовать — это нормально
    }
    
  3. API требует обязательной обработки
    public void transferMoney(Account from, Account to, BigDecimal amount)
         throws InsufficientFundsException {
     // Вызывающий ОБЯЗАН обработать случай недостаточных средств
    }
    

Когда наследоваться от RuntimeException (Unchecked)

Используй unchecked exception, когда:

  1. Ошибка программирования (баг, неправильные аргументы)
    public User getUser(Long id) {
     if (id == null) {
         throw new IllegalArgumentException("ID cannot be null");
     }
     // ...
    }
    
  2. Восстановление невозможно
    public Config loadConfig(String path) {
     if (!Files.exists(path)) {
         throw new ConfigurationException("Config file missing: " + path);
         // Программа не может работать без конфигурации
     }
    }
    
  3. 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 повсюду) Низкая

Типичные ошибки

  1. 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)
  • RustResult<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) исключений]]