← Назад к вопросам

Нравятся ли тебе проверяемые исключения

2.0 Middle🔥 121 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Нравятся ли мне проверяемые исключения

Нет, я критик проверяемых исключений (checked exceptions). Они создают больше проблем, чем решают. Вот мой объективный анализ.

Что такое проверяемые исключения

Проверяемые исключения — это исключения, которые компилятор заставляет обработать (throws или try-catch):

// IOException — проверяемое исключение
public void readFile(String path) throws IOException {  // ОБЯЗАТЕЛЬНО throws
    FileInputStream fis = new FileInputStream(path);
    // ...
}

// Вызывающий код ОБЯЗАН обработать
public void process() throws IOException {  // или catch
    readFile("data.txt");
}

Проблемы проверяемых исключений

1. Boilerplate код (много бесполезного кода)

// Плохо: throws везде
public void method1() throws IOException { ... }
public void method2() throws IOException { ... }
public void method3() throws IOException { ... }
public void method4() throws IOException { ... }
public void main() throws IOException { ... }

// Или try-catch везде
public void method1() {
    try {
        readFile();
    } catch (IOException e) {
        e.printStackTrace();  // Игнорируем
    }
}

2. Нарушает инкапсуляцию

// IOException из implementation деталей утекает в публичный API
public interface DataService {
    String getData() throws IOException;  // Внутренняя деталь!
}

// Если переедем на другой источник данных без IOException:
public class NewDataService implements DataService {
    @Override
    public String getData() throws IOException {  // Вынуждены объявлять
        // Может не вбросить IOException, но объявляем
        return "data";
    }
}

3. Вынуждает bad practice

// Проблема 1: Silent failures
try {
    connect();
} catch (SQLException e) {
    // Пусто! (bad practice, но проще чем throws везде)
}

// Проблема 2: Exception wrapping
try {
    readFromDatabase();
} catch (SQLException e) {
    // Приходится оборачивать
    throw new RuntimeException("DB error", e);
}

4. Усложняет рефакторинг

// Добавили новый throws в helper method
private void helper() throws IOException {
    // ...
}

// Теперь ВСЕ методы которые вызывают helper(), должны его пробросить
public void process1() throws IOException {  // Раньше не было!
    helper();
}

public void process2() throws IOException {  // Раньше не было!
    helper();
}

// Если это интерфейс — нужно менять сигнатуры везде

5. Непредсказуемость

// Какие исключения может выбросить Stream API?
List<String> names = Arrays.asList("a", "b", "c");
names.stream()
    .map(s -> s.toUpperCase())  // Unchecked
    .forEach(System.out::println);  // Может быть IOException? Неизвестно!

// А с функциональным кодом еще сложнее
Functions.compose(
    readFile(),      // throws IOException
    parseJSON(),     // throws ParseException
    printResult()    // throws SQLException
).execute();  // Что здесь может быть? Неизвестно

Примеры: Как это выглядит в реальности

// Spring (современный фреймворк) отказался от checked exceptions
@Repository
public class UserRepository {
    public User findById(Long id) {
        // SQLException оборачивается в DataAccessException (unchecked)
        try {
            return db.execute("SELECT * FROM users WHERE id = ?", id);
        } catch (SQLException e) {
            throw new DataAccessException("DB error", e);  // Unchecked!
        }
    }
}

// Java NIO 2 (современное API) использует unchecked
Path path = Paths.get("data.txt");
byte[] data = Files.readAllBytes(path);  // Не нужен throws IOException!

Когда checked exceptions имеют смысл

И всё же есть редкие случаи, когда они полезны:

1. Особые ошибки, требующие действия

// Смысл: Клиент ДОЛЖЕН обработать (retry, ask user, etc)
public void uploadFile(File file) throws FileQuotaExceededException {
    if (user.getQuota() < file.size()) {
        throw new FileQuotaExceededException("Квота исчерпана");
    }
}

// Вызывающий код ДОЛЖЕН принять решение
try {
    uploadFile(largeFile);
} catch (FileQuotaExceededException e) {
    // Предложить upgrade, или delete old files
    user.upgradeQuota();
}

2. Критичные операции

// Смысл: Ошибка критична, нужно явно её видеть
public void transferMoney(Account from, Account to, BigDecimal amount) 
    throws InsufficientFundsException {
    if (from.getBalance() < amount) {
        throw new InsufficientFundsException();
    }
    // ...
}

Лучший подход: Unchecked exceptions

// 1. Используй RuntimeException для непредвиденных ошибок
public class DataAccessException extends RuntimeException {}

// 2. Оборачивай checked в unchecked на слое приложения
public class UserService {
    public User findById(Long id) {  // Нет throws!
        try {
            return repository.findById(id);
        } catch (SQLException e) {
            throw new DataAccessException("DB error", e);  // Unchecked
        }
    }
}

// 3. Клиент может catch если хочет, может не ловить
public void process(Long userId) {
    try {
        User user = service.findById(userId);
    } catch (DataAccessException e) {  // Optional!
        logger.error("Cannot find user", e);
    }
}

Исторический контекст

Когда создавали Java (1995), думали, что checked exceptions помогут:

  • Обрабатывать ошибки явно
  • Улучшить надёжность кода

Однако практика показала:

  • Люди просто пробрасывают throws везде
  • Или делают пустые catch блоки
  • Это создаёт фальшивое чувство безопасности

Эволюция языков

Java (1995): Checked exceptions обязательны
  ↓
C# (.NET): Решили НЕ делать checked exceptions
  ↓
Java 8+: Функциональное программирование (Stream API) не использует checked
  ↓
Kotlin: Нет checked exceptions (учитель Java)
  ↓
Rust: Result<T, E> вместо исключений (явная обработка)

Best Practice на 2024+

// 1. Не создавай новые checked exceptions
// 2. Оборачивай existing checked в unchecked
// 3. Используй Optional вместо null throws
// 4. Логируй ошибки с полным stacktrace

public class AppException extends RuntimeException {}

public void operation() {  // Нет throws!
    try {
        criticalLogic();
    } catch (SQLException | IOException e) {
        logger.error("Operation failed", e);  // Логируем
        throw new AppException("Операция не удалась", e);  // Пробрасываем unchecked
    }
}

Мнение экспертов

  • Joshua Bloch (Effective Java): "Checked exceptions могут улучшить надёжность, но при чрезмерном использовании приводят к bad API"
  • Bruce Eckel: "Checked exceptions — ошибка дизайна Java"
  • Anders Hejlsberg (создатель C#): "Мы намеренно не делали checked exceptions"

Вывод

Я НЕ люблю проверяемые исключения по практическим причинам:

  1. Boilerplate код — throws везде
  2. Bad practice — пустые catch блоки или throw RuntimeException
  3. Нарушение инкапсуляции — внутренние ошибки в API
  4. Усложняет рефакторинг — изменение implementation меняет сигнатуры
  5. Фальшивое чувство безопасности — люди их игнорируют

Мой подход:

  • Используй unchecked exceptions (RuntimeException)
  • Оборачивай checked в unchecked на слое приложения
  • Логируй ошибки правильно
  • Используй Optional для ожидаемых ошибок
  • Создавай собственные application-specific исключения

Это подход современных фреймворков (Spring, Quarkus) и языков (Kotlin, Go, Rust).