Разберите последствия выполнения метода: какое исключение попадёт в catch и где окажется ошибка закрытия ресурса?
class Demo {
static class R implements AutoCloseable {
void use() { throw new IllegalStateException("body"); }
@Override public void close() {
throw new IllegalArgumentException("close");
}
}
public static void main(String[] args) {
try (R r = new R()) {
r.use();
} catch (Exception e) {
System.out.println(e.getClass().getSimpleName());
System.out.println(e.getSuppressed().length);
}
}
}
В catch попадёт IllegalStateException, возникшее в теле try. Исключение IllegalArgumentException из close() не потеряется: оно будет добавлено к основному через getSuppressed() и поэтому длина массива составит 1.
До появления try-with-resources в Java 7 освобождение ресурсов обычно выполняли в finally. Такой код был многословным и создавал риск утечки ресурса или потери исходного исключения, если одновременно завершались с ошибкой и основная операция, и закрытие.
Конструкция try-with-resources автоматизировала вызов close() для объектов AutoCloseable. Правило подавленных исключений решило конфликт между ошибкой основной операции и ошибкой освобождения ресурса: исходная ошибка сохраняется как главная.
В примере ошибка возникает сначала в use(), а затем при автоматическом закрытии r. Если обработать эти события как обычную последовательность вызовов в finally, можно случайно заменить более важную ошибку тела метода исключением из close().
Такое замещение осложняет диагностику: стек вызовов будет указывать на закрытие ресурса, хотя реальная причина сбоя находится в основной операции. Кроме того, без явного просмотра подавленных исключений часть диагностической информации останется незамеченной.
Ресурс из заголовка try закрывается автоматически при выходе из блока — как при обычном завершении, так и при исключении. Для одного ресурса порядок фактически эквивалентен вызову close() в finally, но try-with-resources дополнительно определяет правила связывания исключений.
Если тело try завершилось исключением, оно становится основным. Исключение из close() добавляется к нему методом addSuppressed() и доступно через getSuppressed(). Поэтому e.getClass() возвращает IllegalStateException, а e.getSuppressed()[0] содержит IllegalArgumentException.
Если тело try завершилось успешно, но close() выбросил исключение, исключение закрытия становится основным и передаётся дальше. При нескольких ресурсах они закрываются в обратном порядке объявления; если несколько закрытий завершились ошибкой, первое возникшее при закрытии обычно становится основным, а последующие добавляются как подавленные.
Минимальный способ вывести полную картину:
Подавленное исключение не означает, что ошибка несущественна. Оно означает, что приоритет отдан исключению, возникшему в основной операции. При логировании нужно сохранять сам основной объект исключения, а не только getMessage(), иначе можно потерять цепочку подавленных ошибок.
AutoCloseable допускает объявление throws Exception, тогда как Closeable предназначен прежде всего для потоков ввода-вывода и ограничивает close() исключением IOException. Объект ресурса должен быть корректно закрываемым и обычно освобождать собственные внешние ресурсы идемпотентно, чтобы повторное закрытие не приводило к новым проблемам.
Сервис читает данные из файла и при завершении закрывает поток. Во время чтения возникает ошибка повреждённого формата, а файловая система дополнительно возвращает ошибку при закрытии дескриптора.
Вариант с ручным finally проще написать на первый взгляд, но небрежная реализация может перезаписать исключение чтения ошибкой закрытия. Ручное сохранение исходной ошибки и добавление подавленной требует больше кода и легко реализуется непоследовательно.
Вариант с игнорированием ошибки close() ещё хуже: он скрывает проблемы с файловой системой и может осложнить поиск утечек или повреждения инфраструктуры. Вариант с try-with-resources автоматически сохраняет ошибку чтения главной, а ошибку закрытия — в getSuppressed().
Поэтому выбирают try-with-resources, а на границе приложения логируют исключение целиком. Результат — корректное освобождение ресурса, сохранение первопричины и доступность вторичной ошибки для диагностики.
Что произойдёт, если исключение возникнет только в close()?
Если тело try завершилось нормально, исключение из close() становится основным и выходит из конструкции try-with-resources. Оно может быть перехвачено соответствующим catch или объявлено в throws. Подавленным оно становится только тогда, когда уже существует более раннее основное исключение.
В каком порядке закрываются несколько ресурсов?
Ресурсы закрываются в порядке, обратном объявлению: последний созданный закрывается первым. Это важно для зависимых ресурсов, например для ResultSet, затем PreparedStatement, затем Connection. Если закрывать их в неправильном порядке вручную, зависимый ресурс может попытаться обратиться к уже закрытому владельцу.
Чем отличается Throwable.getSuppressed() от Throwable.getCause()?
getCause() описывает причинную цепочку: одно исключение было причиной другого и обычно задаётся через конструктор или initCause(). getSuppressed() хранит дополнительные исключения, возникшие при обработке уже выбранного основного исключения, например при закрытии ресурса. Эти связи не взаимозаменяемы: подавленная ошибка не становится причиной основной автоматически.