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

Что такое try-with-resources

До Java 7 разработчикам приходилось закрывать каждый ресурс вручную в блоке finally:


🟢 Junior Level

try-with-resources (TWR) — это конструкция языка Java (появившаяся в Java 7), предназначенная для автоматического и гарантированного закрытия внешних ресурсов (файлов, потоков ввода-вывода, сетевых сокетов, подключений к базам данных) после завершения работы с ними.

Главное преимущество

До Java 7 разработчикам приходилось закрывать каждый ресурс вручную в блоке finally:

// ❌ УСТАРЕВШИЙ ПОДХОД (до Java 7): громоздко и легко допустить утечку
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("config.txt"));
    String line = br.readLine();
} finally {
    if (br != null) {
        try {
            br.close(); // Вторичный try-catch прямо внутри finally!
        } catch (IOException e) {
            log.error("Failed to close reader", e);
        }
    }
}

С try-with-resources ресурс объявляется прямо в круглых скобках после оператора try, и метод close() вызывается автоматически:

// ✅ СОВРЕМЕННЫЙ ПОДХОД: лаконично, безопасно, надежно
try (BufferedReader br = new BufferedReader(new FileReader("config.txt"))) {
    String line = br.readLine();
} // Метод br.close() будет вызван автоматически в любой ситуации!

Главное требование к ресурсу

Любой объект, передаваемый в try-with-resources, обязан реализовывать интерфейс java.lang.AutoCloseable (или его более узкий предшественник java.io.Closeable). Если класс не имеет метода close(), компилятор javac не позволит использовать его в TWR.


🟡 Middle Level

Развертывание компилятором (Under the Hood Desugaring)

Конструкция try-with-resources — это синтаксический сахар компилятора javac. В байткоде компилятор разворачивает ее в сложный блок try-catch-finally с сохранением подавленных исключений:

// Исходный код:
try (Resource r = new Resource()) {
    r.doWork();
}

// Примерный эквивалент, генерируемый компилятором в байткоде:
Resource r = new Resource();
Throwable primaryException = null;
try {
    r.doWork();
} catch (Throwable t) {
    primaryException = t;
    throw t;
} finally {
    if (r != null) {
        if (primaryException != null) {
            try {
                r.close();
            } catch (Throwable closeException) {
                // Исключение закрытия прикрепляется к основному как подавленное!
                primaryException.addSuppressed(closeException);
            }
        } else {
            r.close();
        }
    }
}

Механизм Suppressed Exceptions (Подавленные исключения)

В старом блоке finally возникновение ошибки в close() затирало исходную ошибку бизнес-логики. В try-with-resources:

  1. Если в блоке try возникло SQLException (основная причина).
  2. А при закрытии close() возникло IOException.
  3. Исходное SQLException летит дальше как главное, а IOException добавляется в его список подавленных (Suppressed).
  4. Просмотреть их можно вызовом exception.getSuppressed().

Опасность вложенного создания ресурсов (Nested Wrapper Leak)

Распространенная ошибка даже среди опытных разработчиков — вложенная инициализация потоков-оберток в одной строке:

// ❌ ОПАСНО: риск утечки системного дескриптора файла!
try (BufferedWriter writer = new BufferedWriter(new FileWriter("data.txt"))) {
    writer.write("Hello");
}

Почему это опасно: Сначала создается new FileWriter("data.txt") (открывается системный дескриптор файла в ОС). Затем начинает выполняться конструктор new BufferedWriter(...). Если во время работы конструктора BufferedWriter произойдет сбой (например, OutOfMemoryError), ссылка на FileWriter не будет сохранена в управляемую переменную TWR, и метод close() для него никогда не вызовется — возникает утечка файла в операционной системе!

Каноничное безопасное решение:

// ✅ БЕЗОПАСНО: каждый ресурс объявлен раздельно через точку с запятой
try (FileWriter fw = new FileWriter("data.txt");
     BufferedWriter writer = new BufferedWriter(fw)) {
    writer.write("Hello");
}

Улучшение в Java 9: Effectively Final переменные

В Java 7 и 8 переменные обязаны были объявляться непосредственно внутри круглых скобок try(...). Начиная с Java 9, разрешено использовать уже существующие переменные, если они являются final или effectively final:

public void process(Connection connection) throws SQLException {
    // В Java 9+ connection не нужно переобъявлять в блоке try
    try (connection) {
        // connection закроется по завершении блока
    }
}

🔴 Senior Level

Порядок закрытия ресурсов (LIFO)

Если в блоке объявлено несколько ресурсов, их закрытие происходит в порядке, обратном объявлению (LIFO — Last In, First Out):

try (Connection conn = dataSource.getConnection();      // 1-й открыт, 3-й закрыт
     PreparedStatement ps = conn.prepareStatement(sql); // 2-й открыт, 2-й закрыт
     ResultSet rs = ps.executeQuery()) {                // 3-й открыт, 1-й закрыт

    while (rs.next()) { ... }
}

Почему это критично: ResultSet зависит от PreparedStatement, а Statement зависит от Connection. Закрытие Connection раньше, чем ResultSet, привело бы к ошибке в драйвере БД. Принцип стека (LIFO) гарантирует идеальную корректность зависимостей.

Обработка null ресурсов

Если ресурс в круглых скобках оказался равен null (например, метод-фабрика вернул null), try-with-resources не выбрасывает NullPointerException:

  • Сгенерированный байткод выполняет проверку if (resource != null) resource.close().

Полный жизненный цикл выполнения TWR с явными catch и finally

Senior-инженер обязан четко понимать порядок фаз исполнения сложного блока TWR:

[Фаза 1: Инициализация ресурсов слева направо]
                       │
[Фаза 2: Выполнение тела блока try]
                       │
[Фаза 3: Автоматическое закрытие ресурсов в порядке LIFO] (AutoCloseable.close())
                       │
[Фаза 4: Выполнение явного блока catch] (если было исключение)
                       │
[Фаза 5: Выполнение явного блока finally]

Ключевой вывод: Внутри явного блока catch или явного блока finally ресурсы, объявленные в круглых скобках TWR, УЖЕ ЗАКРЫТЫ! Попытка выполнить в блоке catch запрос через rs или conn приведет к SQLException: Connection is closed.


🎯 Шпаргалка для интервью

30-секундный ответ

try-with-resources (Java 7+) — это синтаксическая конструкция, автоматически закрывающая объявленные в круглых скобках ресурсы при выходе из блока try.

Единственное требование к ресурсу — реализация интерфейса java.lang.AutoCloseable.

Главные особенности:

  1. LIFO-порядок закрытия: Ресурсы закрываются в обратном порядке (последний открытый закрывается первым).
  2. Suppressed Exceptions: Исключение из метода close() не затирает исключение из тела try, а прикрепляется к нему как подавленное (addSuppressed()).
  3. Вложенные ресурсы: Обертки потоков (new BufferedWriter(new FileWriter())) нужно объявлять раздельно, иначе при сбое внешнего конструктора внутренний ресурс утечет.
  4. В Java 9+ разрешено передавать effectively final переменные: try (existingResource).

4 каверзных вопроса с ответами

  1. В каком порядке вызываются методы close(), если в заголовке try объявлены 3 ресурса? Ответ: Строго в порядке, обратном их объявлению (LIFO — Last In, First Out). Ресурс, объявленный третьим, закроется первым; ресурс, объявленный первым, закроется последним.

  2. Что произойдет, если ресурс, объявленный в круглых скобках try-with-resources, равен null? Ответ: Ничего страшного не произойдет, NullPointerException выброшен не будет. Байткод, сгенерированный компилятором, содержит предварительную проверку if (resource != null) перед вызовом close().

  3. Будут ли ресурсы уже закрыты в момент входа в явный блок catch? Ответ: Да. Автоматическое закрытие всех ресурсов (вызов AutoCloseable.close()) происходит сразу после выхода из блока try, строго до передачи управления в явные блоки catch и finally.

  4. Чем опасна конструкция try (BufferedReader br = new BufferedReader(new FileReader("file.txt")))? Ответ: Это уязвимость утечки ресурсов. Сначала вызывается new FileReader(), открывающий дескриптор файла. Если конструктор BufferedReader упадет (например, нехватка памяти OutOfMemoryError), ссылка на FileReader не попадет под управление TWR и никогда не будет закрыта. Ресурсы обязаны объявляться отдельными переменными.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «В try-with-resources можно передавать любой объект, Java закроет его через рефлексию» (объект обязан реализовывать интерфейс AutoCloseable).
  • ❌ «Исключение из метода close() затирает ошибку из тела try» (в TWR используется механизм Suppressed Exceptions, сохраняющий оба сбоя).
  • ❌ «Ресурсы закрываются в том же порядке, в котором открывались» (ресурсы закрываются в обратном порядке — LIFO).
  • ❌ «Если переменная ресурса равна null, TWR упадет с NullPointerException» (TWR безопасно игнорирует null).

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