⚡ Розділ 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 SQL-запит через 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).

Пов’язані теми