Що таке 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 надає два методи:
public final synchronized void addSuppressed(Throwable exception)— прикріплює пригнічений виняток до поточного.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:
- Ледача алокація: Доти, доки
addSuppressed()не викликано, список ініціалізовано легковажною константоюCollections.emptyList(), щоб не витрачати пам’ять купи на порожні списки в мільйонах звичайних винятків. Під час першого виклику створюєтьсяArrayList(1). - Захист від самопригнічення (Self-Suppression): Виклик
e.addSuppressed(e)негайно викидаєIllegalArgumentException: Self-suppression not permitted. - Захист від 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:
- Список
suppressedExceptionsініціалізується спеціальним sentinel-значеннямnull. - Наступні виклики
addSuppressed(t)перетворюються на тихий no-op (виклики ігноруються, масивgetSuppressed()завжди повертає порожній масив довжини 0). - Це застосовується в екстремально високонавантажених системах (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!
}
Завдяки цьому жодна помилка з паралельних віртуальних потоків не губиться.
Ризики витоків пам’яті та накладні витрати серіалізації
- Memory Leak у Singleton Exceptions: Якщо кешувати об’єкт винятку в
static finalполі і при цьому використовувати його всередині TWR або пакетної обробки, багаторазові викликиaddSuppressed()нескінченно роздуватимуть внутрішнійArrayList, утримуючи в пам’яті купи всі супутні об’єкти та їхні стек-трейси. - Серіалізація через мережу (RMI / Hazelcast / Spring Remoting): Метод
getSuppressed()серіалізується цілком. Якщо основний виняток несе в собі 100 пригнічених винятків, мережею буде передано 100 повних стек-трейсів, що може багаторазово збільшити обсяг пакетів передачі.
🎯 Шпаргалка для інтерв’ю
Питання для швидкої перевірки
- У чому фундаментальна різниця між
getCause()таgetSuppressed()? Відповідь:getCause()виражає вертикальний причинно-наслідковий ланцюжок («помилку загорнуто у більш високорівневу»), аgetSuppressed()виражає горизонтальне накопичення супутніх незалежних помилок, що сталися паралельно або під час спроби звільнення ресурсів уclose(). - Чи працює механізм пригнічених винятків автоматично у звичайному блоці
try-finally? Відповідь: Ні. У звичайномуtry-finallyвиняток із блокуfinallyповністю витісняє виняток із блокуtry, затираючи його. Автоматичне додавання черезaddSuppressed()генерується компілятором виключно для конструкціїtry-with-resources. - Що станеться, якщо викликати
e.addSuppressed(e)? Відповідь: Буде викинутоIllegalArgumentExceptionіз повідомленнямSelf-suppression not permitted. Виняток не може пригнічувати сам себе. - Що відбувається під час виклику
addSuppressed(), якщо виняток було створено з параметромenableSuppression = false? Відповідь: Виклик методу буде проігноровано (no-op). Виняток не буде додано, а методgetSuppressed()продовжить повертати порожній масив.
Типові помилки та Red Flags
- ❌ Твердження: “Suppressed exceptions працюють самі по собі в будь-якому блоці try-catch-finally”: Звичайний
finallyне має механізму автоматичного зв’язування, це фіча компілятора для TWR. - ❌ Плутанина між
causeтаsuppressedпід час пояснення стек-трейсу: Нерозуміння семантики рядківCaused by:таSuppressed:. - ❌ Додавання пригнічених винятків до singleton-винятків: Призводить до витоку пам’яті через нескінченне зростання внутрішнього списку.
- ❌ Спроба передати
nullу методaddSuppressed(): Викликає негайнийNullPointerException.