⚡ Раздел 7 · Вопрос #24

Что такое suppressed exceptions

Механизм был создан специально для конструкции 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.

Связанные темы