⚡ Розділ 7 · Питання #24

Що таке suppressed винятки

Механізм був створений спеціально для конструкції try-with-resources (TWR): якщо помилка виникає в основному тілі блоку try, а потім ще одна помилка вилітає під час автоматичног...


🟢 Junior Level

Коротка відповідь для співбесіди (30 секунд)

Пригнічені винятки (Suppressed Exceptions) — це механізм Java (запроваджений у Java 7), який дозволяє одному головному винятку містити в собі масив супутніх винятків, що виникли під час звільнення ресурсів або аварійного виходу.

Механізм був створений спеціально для конструкції try-with-resources (TWR): якщо помилка виникає в основному тілі блоку try, а потім ще одна помилка вилітає під час автоматичного виклику close(), первинний виняток із блоку try залишається основним, а виняток із методу close() прикріплюється до нього як пригнічений. Це повністю усуває історичну проблему старих блоків finally, де вторинна помилка затирала першопричину збою.


Методи класу Throwable

Для роботи з пригніченими винятками клас java.lang.Throwable надає два методи:

  1. public final synchronized void addSuppressed(Throwable exception) — прикріплює пригнічений виняток до поточного.
  2. public final synchronized Throwable[] getSuppressed() — повертає масив усіх прикріплених пригнічених винятків (порожній масив, якщо пригнічених помилок немає).

Базовий приклад на Java 21

public class SuppressedDemo {
    static class BrokenResource implements AutoCloseable {
        public void execute() {
            throw new IllegalStateException("Primary business failure in try");
        }

        @Override
        public void close() {
            throw new RuntimeException("Secondary failure during close()");
        }
    }

    public static void main(String[] args) {
        try (var res = new BrokenResource()) {
            res.execute();
        } catch (Exception ex) {
            System.err.println("Main exception: " + ex.getMessage());

            Throwable[] suppressed = ex.getSuppressed();
            System.err.println("Suppressed count: " + suppressed.length);
            for (Throwable s : suppressed) {
                System.err.println(" -> Suppressed: " + s.getMessage());
            }
        }
    }
}

Вивід у консолі:

Main exception: Primary business failure in try
Suppressed count: 1
 -> Suppressed: Secondary failure during close()

У виводі логів ex.printStackTrace() пригнічені помилки відображаються зі спеціальним префіксом Suppressed::

java.lang.IllegalStateException: Primary business failure in try
    at SuppressedDemo$BrokenResource.execute(SuppressedDemo.java:4)
    at SuppressedDemo.main(SuppressedDemo.java:15)
    Suppressed: java.lang.RuntimeException: Secondary failure during close()
        at SuppressedDemo$BrokenResource.close(SuppressedDemo.java:9)
        ... 1 more

🟡 Middle Level

Різниця між Exception Chaining (cause) та Suppressed Exceptions

Критерій Exception Chaining (getCause()) Suppressed Exceptions (getSuppressed())
Відношення подій Причинно-наслідковий зв’язок (Вертикальний): “Помилка А сталася через помилку Б” Супутні збої (Горизонтальний): “Сталася помилка А, а під час спроби очищення додатково виникла помилка Б”
Кількість помилок Рівно одна батьківська ланка (cause) Масив довільної кількості супутніх помилок
Як створюється Конструктор new Exception("msg", cause) Метод primary.addSuppressed(secondary)
Відображення в логах Caused by: ... Suppressed: ...

Внутрішня будова в класі Throwable

Усередині java.lang.Throwable список пригнічених винятків зберігається в полі:

private List<Throwable> suppressedExceptions = Collections.emptyList();

Тонкощі контракту addSuppressed:

  1. Ледача алокація: Доти, доки addSuppressed() не викликано, список ініціалізовано легковажною константою Collections.emptyList(), щоб не витрачати пам’ять купи на порожні списки в мільйонах звичайних винятків. Під час першого виклику створюється ArrayList(1).
  2. Захист від самопригнічення (Self-Suppression): Виклик e.addSuppressed(e) негайно викидає IllegalArgumentException: Self-suppression not permitted.
  3. Захист від null: Виклик e.addSuppressed(null) викидає NullPointerException: Cannot suppress a null exception..

Ручне використання патерну для пакетної обробки (Batch Processing)

Хоча TWR керує addSuppressed автоматично, розробники можуть використовувати цей механізм вручну для акумуляції помилок у циклічних операціях:

import java.util.List;

public class BatchProcessor {

    public void processAll(List<Runnable> tasks) {
        ProcessingException batchError = null;

        for (Runnable task : tasks) {
            try {
                task.run();
            } catch (Exception ex) {
                if (batchError == null) {
                    batchError = new ProcessingException("Batch execution failed with one or more errors");
                }
                // Акумулюємо помилки замість переривання циклу
                batchError.addSuppressed(ex);
            }
        }

        if (batchError != null) {
            throw batchError;
        }
    }

    public static class ProcessingException extends RuntimeException {
        public ProcessingException(String message) { super(message); }
    }
}

🔴 Senior Level

Прапорець enableSuppression у 4-аргументному конструкторі Java 7+

У Java 7 до класу Throwable було додано захищений конструктор:

protected Throwable(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace)

Якщо передати enableSuppression = false:

  1. Список suppressedExceptions ініціалізується спеціальним sentinel-значенням null.
  2. Наступні виклики addSuppressed(t) перетворюються на тихий no-op (виклики ігноруються, масив getSuppressed() завжди повертає порожній масив довжини 0).
  3. Це застосовується в екстремально високонавантажених системах (Highload / Ultra-low latency) для створення легковажних винятків без алокацій внутрішніх структур даних.

Structured Concurrency (Java 21 / JEP 453) та Suppressed Exceptions

У Java 21 механізм пригнічення винятків став фундаментом для структурованої багатопоточності (Structured Concurrency з використанням StructuredTaskScope):

// Java 21 Preview / Structured Concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Supplier<String> user = scope.fork(() -> fetchUser());
    Supplier<Integer> balance = scope.fork(() -> fetchBalance());

    scope.join();           // Очікування завершення всіх підзадач
    scope.throwIfFailed();  // Викидає виняток першої підзадачі, що впала!

    // Якщо кілька фонових завдань упадуть одночасно, перша помилка стане Primary,
    // а помилки інших паралельних завдань будуть автоматично додані як Suppressed!
}

Завдяки цьому жодна помилка з паралельних віртуальних потоків не губиться.


Ризики витоків пам’яті та накладні витрати серіалізації

  1. Memory Leak у Singleton Exceptions: Якщо кешувати об’єкт винятку в static final полі і при цьому використовувати його всередині TWR або пакетної обробки, багаторазові виклики addSuppressed() нескінченно роздуватимуть внутрішній ArrayList, утримуючи в пам’яті купи всі супутні об’єкти та їхні стек-трейси.
  2. Серіалізація через мережу (RMI / Hazelcast / Spring Remoting): Метод getSuppressed() серіалізується цілком. Якщо основний виняток несе в собі 100 пригнічених винятків, мережею буде передано 100 повних стек-трейсів, що може багаторазово збільшити обсяг пакетів передачі.

🎯 Шпаргалка для інтерв’ю

Питання для швидкої перевірки

  1. У чому фундаментальна різниця між getCause() та getSuppressed()? Відповідь: getCause() виражає вертикальний причинно-наслідковий ланцюжок («помилку загорнуто у більш високорівневу»), а getSuppressed() виражає горизонтальне накопичення супутніх незалежних помилок, що сталися паралельно або під час спроби звільнення ресурсів у close().
  2. Чи працює механізм пригнічених винятків автоматично у звичайному блоці try-finally? Відповідь: Ні. У звичайному try-finally виняток із блоку finally повністю витісняє виняток із блоку try, затираючи його. Автоматичне додавання через addSuppressed() генерується компілятором виключно для конструкції try-with-resources.
  3. Що станеться, якщо викликати e.addSuppressed(e)? Відповідь: Буде викинуто IllegalArgumentException із повідомленням Self-suppression not permitted. Виняток не може пригнічувати сам себе.
  4. Що відбувається під час виклику addSuppressed(), якщо виняток було створено з параметром enableSuppression = false? Відповідь: Виклик методу буде проігноровано (no-op). Виняток не буде додано, а метод getSuppressed() продовжить повертати порожній масив.

Типові помилки та Red Flags

  • ❌ Твердження: “Suppressed exceptions працюють самі по собі в будь-якому блоці try-catch-finally”: Звичайний finally не має механізму автоматичного зв’язування, це фіча компілятора для TWR.
  • ❌ Плутанина між cause та suppressed під час пояснення стек-трейсу: Нерозуміння семантики рядків Caused by: та Suppressed:.
  • ❌ Додавання пригнічених винятків до singleton-винятків: Призводить до витоку пам’яті через нескінченне зростання внутрішнього списку.
  • ❌ Спроба передати null у метод addSuppressed(): Викликає негайний NullPointerException.

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