Питання 1 · Розділ 7

В чому різниця між checked та unchecked винятками?

В Java усі винятки успадковуються від java.lang.Throwable. Винятки поділяються на дві категорії:

Мовні версії: English Russian Ukrainian

Junior Level

Основи

В Java усі винятки успадковуються від java.lang.Throwable. Винятки поділяються на дві категорії:

Checked exceptions (перевірювані):

  • Спадкоємці Exception (крім RuntimeException)
  • Компілятор зобов’язує їх обробляти: або через try-catch, або через throws у сигнатурі метода
  • Приклади: IOException, SQLException, ClassNotFoundException

Unchecked exceptions (неперевірювані):

  • Спадкоємці RuntimeException
  • Компілятор не вимагає їх обробки
  • Приклади: NullPointerException, IllegalArgumentException, IndexOutOfBoundsException

Коли використовувати

  • Checked — для зовнішніх відновлюваних збоїв (файл не знайдено, мережа недоступна)
  • Unchecked — для помилок програмування (null, невірний аргумент) та бізнес-помилок
// Checked — потрібно обробити або оголосити throws
public void readFile() throws IOException {
    Files.readAllLines(Paths.get("file.txt"));
}

// Unchecked — компілятор не вимагає обробки
public void divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("Division by zero");
}

Middle Level

Коли НЕ використовувати checked винятки

  1. Бізнес-логіка – checked винятки ламають композицію (потрібно оголошувати throws у кожному методі вгору по стеку)
  2. REST-контролери – Spring сам обробить unchecked винятки через @ExceptionHandler
  3. Stream API – лямбди не підтримують checked винятки (Function<T,R> не оголошує throws)
  4. CompletableFuture – винятки загортаються в CompletionException, checked втрачають сенс

Механізм роботи

JVM не примусово перевіряє типи винятків у рантаймі – розділення checked/unchecked працює лише на етапі компіляції. У байткоді усі винятки обробляються однаково. Інструкція athrow просто викидає об’єкт.

Exception Translation

Гарна практика – перехоплювати checked винятки на межі шарів і загортати їх у доменні unchecked винятки:

public class UserRepository {
    public User findById(Long id) {
        try {
            return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?",
                (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")), id);
        } catch (DataAccessException e) {
            throw new UserNotFoundException("User not found: " + id, e);
        }
    }
}

Multi-catch (Java 7+)

try {
    // код
} catch (IOException | SQLException e) {
    log.error("Resource error", e);
    throw new ServiceException("Failed", e);
}

Чому сучасні мови відмовилися від checked exceptions

Kotlin і Scala не мають checked exceptions тому що:

  1. Забруднення сигнатур — додавання throws вимагає зміни всіх методів вгору по стеку
  2. Несумісність з FP — функціональні інтерфейси (Stream API, CompletableFuture) не підтримують checked винятки

Senior Level

Under the Hood: байт-код та JVM

Атрибут Exceptions у .class файлі зберігає інформацію про checked винятки, але JVM використовує його лише для верифікації, а не для контролю в рантаймі.

Sneaky Throws — обхід компілятора

Оскільки JVM не перевіряє типи винятків, можна кинути checked виняток без throws:

@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
    throw (E) e;
}
// Використання: sneakyThrow(new IOException()); — компілюється без throws!

Lombok @SneakyThrows використовує той самий механізм.

Стирання типів: дженерик E стирається до Throwable у рантаймі. Компілятор думає, що метод кидає E (параметр метода), а JVM викидає реальний тип. Тому throws не потрібен у сигнатурі.

⚠️ Це небезпечно: викликач не знає, що метод може кинути IOException. Використовуйте лише всередині фреймворків, ніколи в бізнес-логіці.

Продуктивність у Highload

fillInStackTrace() — найдорожча частина винятку. JVM проходить по всьому стеку викликів. При 100k+ RPS створення винятків стає bottleneck.

Оптимізація: для винятків-сигналів вимкніть стек-трейс:

public class FastException extends RuntimeException {
    public FastException(String message) {
        super(message, null, false, false); // writableStackTrace = false
    }
}

Це прискорює створення у 10-50 разів.

Архітектурний вибір

Checked винятки хороші лише для зовнішніх відновлюваних збоїв у бібліотеках низького рівня:

  • InterruptedException — Java змушує обробляти переривання, щоб не зламати управління потоками

Unchecked винятки переважні тому що:

  • Чистота бізнес-логіки без try-catch шуму
  • Централізована обробка через @ControllerAdvice
  • Сумісність з Reactive programming

Edge Cases

  • Exception Translation на межі шарів — DAO перехоплює SQLException і загортає у доменний unchecked виняток
  • UncaughtExceptionHandler — завжди встановлюйте глобальний обробник для потоків
  • Stack Trace Filtering — у Highload обрізайте стек-трейс або використовуйте JSON формат (Logstash)

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Checked exceptions — спадкоємці Exception (крім RuntimeException), компілятор зобов’язує обробити
  • Unchecked exceptions — спадкоємці RuntimeException, компілятор не вимагає обробки
  • Checked — для зовнішніх відновлюваних збоїв (файл, мережа, БД)
  • Unchecked — для помилок програмування та бізнес-помилок
  • JVM не перевіряє типи винятків у рантаймі — розділення лише на рівні компілятора
  • Kotlin і Scala відмовилися від checked через забруднення сигнатур та несумісність з FP
  • sneakyThrow дозволяє обійти перевірку компілятора через стирання типів
  • Для highload вимикайте стек-трейс (writableStackTrace = false)

Часті уточнюючі запитання:

  • Чому сучасні мови відмовилися від checked exceptions? — Забруднення сигнатур, несумісність з лямбдами та Stream API
  • Що таке exception translation? — Перехоплення checked винятку на межі шару і загортання у доменний unchecked
  • Як працює sneakyThrow? — Дженерик E extends Throwable стирається до Throwable, JVM не перевіряє тип
  • Коли checked виняток виправданий? — Для зовнішніх відновлюваних збоїв у бібліотеках низького рівня (IOException, InterruptedException)

Червоні прапорці (НЕ говорити):

  • “Я завжди використовую checked exceptions, щоб змусити колег обробляти помилки” — це антипатерн для бізнес-логіки
  • “JVM перевіряє checked exceptions у рантаймі” — ні, лише компілятор
  • “Kotlin не підтримує винятки взагалі” — Kotlin підтримує unchecked, просто прибрав checked

Пов’язані теми:

  • [[Що таке checked виняток і коли його використовувати]]
  • [[Що таке unchecked виняток (Runtime Exception)]]
  • [[Які винятки потрібно обробляти обов’язково]]
  • [[В чому різниця між Error та Exception]]
  • [[Що таке Throwable]]