Программирование JavaИсключенияJava-разработчик серверных приложений

Разберите последствия выполнения метода: какое исключение попадёт в catch и где окажется ошибка закрытия ре...

Разберите последствия выполнения метода: какое исключение попадёт в 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);
        }
    }
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

В 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() выбросил исключение, исключение закрытия становится основным и передаётся дальше. При нескольких ресурсах они закрываются в обратном порядке объявления; если несколько закрытий завершились ошибкой, первое возникшее при закрытии обычно становится основным, а последующие добавляются как подавленные.

Минимальный способ вывести полную картину:

try (R r = new R()) { r.use(); } catch (Exception e) { System.out.println(e); // основная ошибка for (Throwable s : e.getSuppressed()) { System.out.println(s); // ошибки закрытия } }

Подавленное исключение не означает, что ошибка несущественна. Оно означает, что приоритет отдан исключению, возникшему в основной операции. При логировании нужно сохранять сам основной объект исключения, а не только getMessage(), иначе можно потерять цепочку подавленных ошибок.

AutoCloseable допускает объявление throws Exception, тогда как Closeable предназначен прежде всего для потоков ввода-вывода и ограничивает close() исключением IOException. Объект ресурса должен быть корректно закрываемым и обычно освобождать собственные внешние ресурсы идемпотентно, чтобы повторное закрытие не приводило к новым проблемам.

Ситуация из практики

Сервис читает данные из файла и при завершении закрывает поток. Во время чтения возникает ошибка повреждённого формата, а файловая система дополнительно возвращает ошибку при закрытии дескриптора.

Вариант с ручным finally проще написать на первый взгляд, но небрежная реализация может перезаписать исключение чтения ошибкой закрытия. Ручное сохранение исходной ошибки и добавление подавленной требует больше кода и легко реализуется непоследовательно.

Вариант с игнорированием ошибки close() ещё хуже: он скрывает проблемы с файловой системой и может осложнить поиск утечек или повреждения инфраструктуры. Вариант с try-with-resources автоматически сохраняет ошибку чтения главной, а ошибку закрытия — в getSuppressed().

Поэтому выбирают try-with-resources, а на границе приложения логируют исключение целиком. Результат — корректное освобождение ресурса, сохранение первопричины и доступность вторичной ошибки для диагностики.

Что кандидаты часто упускают

  1. Что произойдёт, если исключение возникнет только в close()?

    Если тело try завершилось нормально, исключение из close() становится основным и выходит из конструкции try-with-resources. Оно может быть перехвачено соответствующим catch или объявлено в throws. Подавленным оно становится только тогда, когда уже существует более раннее основное исключение.

  2. В каком порядке закрываются несколько ресурсов?

    Ресурсы закрываются в порядке, обратном объявлению: последний созданный закрывается первым. Это важно для зависимых ресурсов, например для ResultSet, затем PreparedStatement, затем Connection. Если закрывать их в неправильном порядке вручную, зависимый ресурс может попытаться обратиться к уже закрытому владельцу.

  3. Чем отличается Throwable.getSuppressed() от Throwable.getCause()?

    getCause() описывает причинную цепочку: одно исключение было причиной другого и обычно задаётся через конструктор или initCause(). getSuppressed() хранит дополнительные исключения, возникшие при обработке уже выбранного основного исключения, например при закрытии ресурса. Эти связи не взаимозаменяемы: подавленная ошибка не становится причиной основной автоматически.