Проверьте, будет ли вызван close , если конструктор ресурса завершился исключением: пример с кодом

Проверьте, будет ли вызван close(), если конструктор ресурса завершился исключением:

class Demo {
    static class R implements AutoCloseable {
        R() {
            System.out.println("construct");
            throw new RuntimeException("init");
        }

        @Override
        public void close() {
            System.out.println("close");
        }
    }

    public static void main(String[] args) {
        try (R r = new R()) {
        } catch (RuntimeException e) {
            System.out.println("caught");
        }
    }
}
Проходите собеседования с ИИ помощником Hintsage

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

close() вызван не будет. Выражение new R() не завершилось успешно, поэтому ресурс не считается созданным и не регистрируется механизмом try-with-resources. В консоль будут выведены construct, затем caught.

Исторический контекст

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

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

Постановка проблемы

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

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

Подробное решение

Ресурс в заголовке try-with-resources регистрируется для закрытия только после того, как выражение ресурса завершилось нормально. В данном примере конструктор печатает construct, выбрасывает RuntimeException, а тело try вообще не начинает выполняться.

Сгенерированная логика эквивалентна следующей по смыслу последовательности: сначала вычислить new R(), затем сохранить успешно полученный объект, выполнить тело try и только после этого закрыть ресурс. На этапе вычисления конструктора до сохранения объекта выполнение прерывается, поэтому вызова close() нет.

Код напечатает:

construct caught

Если ресурсов несколько, исключение при создании очередного ресурса не закрывает этот незавершённо созданный ресурс, но ранее успешно созданные ресурсы закрываются в обратном порядке. Это различие важно: try-with-resources управляет временем жизни успешно приобретённых объектов, а не откатывает произвольные побочные эффекты конструктора.

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

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

Сервис создаёт объект клиента базы данных. Конструктор сначала открывает нативное соединение, затем настраивает параметры, но настройка может завершиться исключением. Разработчик помещает создание клиента в try-with-resources и ожидает, что close() устранит утечку.

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

Выбранное решение — сделать приобретение соединения отдельной операцией с локальным дескриптором и явно закрывать его в обработчике ошибки до завершения фабрики. Только полностью инициализированный клиент возвращается вызывающему коду и передаётся в try-with-resources. Это отделяет откат неудачного создания от обычного жизненного цикла готового ресурса.

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

1. Что должен делать конструктор, если он успел захватить внешний ресурс, а затем выбросил исключение?

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

2. Закроется ли предыдущий ресурс, если конструктор следующего ресурса завершился исключением?

Да, каждый ресурс, который был успешно создан ранее, закрывается. Незавершённое создание текущего ресурса не приводит к вызову его close(), но ранее зарегистрированные ресурсы закрываются в обратном порядке. Если закрытие добавляет новые ошибки, они обрабатываются по правилам подавленных исключений.

3. Меняет ли ситуацию фабричный метод, возвращающий уже созданный ресурс?

Нет. Для try-with-resources неважно, получен объект через конструктор или фабричный метод. Если метод завершился исключением, ресурс не регистрируется; если метод нормально вернул объект, он регистрируется, и затем будет вызван его close() — в том числе для допустимого null по специальным правилам try-with-resources вызов закрытия в таком случае не выполняется.