⚡ Розділ 7 · Питання #10

Які вимоги до ресурсів в try-with-resources

Для того щоб об'єкт можна було використати в конструкції try-with-resources, до нього висувається одна сувора синтаксична вимога на рівні компілятора Java:


🟢 Junior Level

Для того щоб об’єкт можна було використати в конструкції try-with-resources, до нього висувається одна сувора синтаксична вимога на рівні компілятора Java:

Тип ресурсу зобов’язаний реалізовувати інтерфейс java.lang.AutoCloseable (або його дочірній інтерфейс java.io.Closeable).

Якщо клас не реалізує AutoCloseable, компілятор javac завершить збірку з помилкою: The resource type ... does not implement java.lang.AutoCloseable.

Базовий приклад

// Власний ресурс, придатний для використання в TWR
public class DatabaseSession implements AutoCloseable {

    public void executeQuery(String sql) {
        System.out.println("Executing: " + sql);
    }

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

Використання в блоці:

try (DatabaseSession session = new DatabaseSession()) {
    session.executeQuery("SELECT * FROM users");
} // Тут компілятор автоматично викличе session.close()

🟡 Middle Level

4 інженерні вимоги до якісної реалізації методу close()

Хоча компілятор перевіряє виключно наявність інтерфейсу AutoCloseable, до надійної реалізації методу close() висуваються критичні вимоги:

1. Ідемпотентність (Idempotency)

Повторний виклик методу close() не повинен призводити до помилки або повторного вивільнення ресурсів:

  • Перший виклик вивільняє ресурс.
  • Усі наступні виклики тихо ігноруються (no-op).
  • Для багатопотокових ресурсів стан закриття захищають за допомогою AtomicBoolean:
    private final AtomicBoolean closed = new AtomicBoolean(false);
    
    @Override
    public void close() {
        if (closed.compareAndSet(false, true)) {
            // Реальне вивільнення файлового дескриптора або сокета
        }
    }
    

2. Звуження сигнатури throws (Exception Narrowing)

Інтерфейс AutoCloseable оголошує метод: void close() throws Exception;

Реалізація в прикладному класі наполегливо рекомендує звузити тип винятку (або прибрати throws взагалі, якщо метод закривається безвідмовно):

// ✅ ДОБРЕ: метод оголошує конкретний IOException або взагалі нічого не кидає
public class MemoryBuffer implements AutoCloseable {
    @Override
    public void close() { 
        // Не кидає checked-винятків, клієнту не потрібен зовнішній try-catch навколо TWR!
    }
}

Якщо залишити throws Exception, викликаючий код буде змушений писати catch (Exception e), перехоплюючи зайві типи помилок.

3. Швидкодія та відсутність блокувань

Метод close() не повинен виконувати важкі мережеві запити, тривалі транзакції або блокуватися без таймаутів, оскільки він виконується в критичній фазі завершення потоку.

4. Безпечне завершення при аварійному стані

Якщо в тілі блоку try сталася аварія, метод close() зобов’язаний спробувати коректно вивільнити нативні дескриптори, навіть якщо внутрішній стан об’єкта частково пошкоджений.


🔴 Senior Level

Вимоги Java 9+: змінні мають бути Effectively Final

Починаючи з Java 9, у круглих дужках TWR дозволено передавати посилання на вже оголошені змінні:

final Resource r1 = new Resource();
Resource r2 = new Resource(); // effectively final

try (r1; r2) {
    // робота з ресурсами
}

Критичне правило: змінні зобов’язані бути final або effectively final (значення посилання не перезаписується після ініціалізації). Під капотом JVM: Компілятор генерує приховану локальну синтетичну змінну всередині байткоду методу (final Resource $r1 = r1;). Це захищає від ситуації, коли інший потік міг би підмінити посилання на об’єкт під час виконання тіла блоку try.

Проблема «Частково сконструйованого ресурсу» (Constructor Failure)

Якщо конструктор ресурсу захоплює нативний дескриптор в ОС, а потім зазнає аварії до завершення ініціалізації:

public class BrokenResource implements AutoCloseable {
    private final FileDescriptor fd;

    public BrokenResource(String path) {
        this.fd = openNativeFile(path); // 1. Нативний ресурс захоплено!
        throw new RuntimeException("Crash"); // 2. Аварія в конструкторі!
    }

    @Override
    public void close() {
        closeNativeFile(fd);
    }
}

Наслідки: Оскільки конструктор завершився винятком, посилання на об’єкт не було повернене, змінна в try (...) не ініціалізувалася, і TWR ніколи не викличе метод close()! Нативний дескриптор назавжди витече в операційну систему.

Senior-рішення: Фабричний метод або двоетапна ініціалізація:

public static SafeResource open(String path) {
    FileDescriptor fd = openNativeFile(path);
    try {
        return new SafeResource(fd);
    } catch (Throwable t) {
        closeNativeFile(fd); // Гарантоване очищення при збої конструктора
        throw t;
    }
}

Пули з’єднань (HikariCP / Apache Commons DBCP)

У високонавантажених корпоративних системах створення фізичного TCP-з’єднання з базою даних займає 20–50 мс. Тому екземпляри java.sql.Connection, які видає HikariCP, є проксі-обгортками (ProxyConnection):

  • Вони реалізують AutoCloseable.
  • При виклику методу connection.close() конструкцією TWR фізичний сокет не закривається; проксі-об’єкт просто повертає з’єднання назад до пулу вільних коннектів HikariCP за долі мікросекунди.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

  • Синтаксична вимога компілятора: Тип ресурсу зобов’язаний реалізовувати інтерфейс java.lang.AutoCloseable (або java.io.Closeable).
  • Інженерні вимоги до методу close():
    1. Ідемпотентність: Повторні виклики close() не повинні призводити до помилок чи винятків.
    2. Звуження сигнатури throws: Не залишати загальний throws Exception, а вказувати конкретні типи (наприклад, IOException) або взагалі прибирати секцію throws, якщо операція закриття безвідмовна.
    3. Неблокуюче виконання: Не допускати зависань у close().
  • Вимога Java 9+: Зовнішні змінні, що передаються в TWR, зобов’язані бути final або effectively final.
  • Для пулів ресурсів (HikariCP) виклик close() у TWR повертає з’єднання до пулу, а не закриває фізичний сокет.

4 каверзних питання з відповідями

  1. Що станеться, якщо клас ресурсу має публічний метод void close(), але не імплементує інтерфейс AutoCloseable? Відповідь: Код не скомпільовано. Java є строго типізованою мовою, і конструкція try-with-resources перевіряє реалізацію інтерфейсу java.lang.AutoCloseable на етапі статичного аналізу типів компілятором javac, а не за допомогою рефлексії під час виконання.

  2. Чому в реалізації методу close() наполегливо рекомендується прибирати або звужувати секцію throws Exception? Відповідь: В інтерфейсі AutoCloseable метод оголошено як void close() throws Exception. Якщо розробник залишить throws Exception, викликаючий код навколо TWR буде зобов’язаний перехоплювати або прокидати найзагальніший Exception, що порушує чіткість обробки помилок. Якщо метод close() не кидає винятків, клієнту не доведеться писати зайві блоки catch.

  3. Чи зобов’язаний метод close() в AutoCloseable бути ідемпотентним за офіційною специфікацією Javadoc? Відповідь: У специфікації AutoCloseable зазначено, що метод рекомендується робити ідемпотентним, проте суворої вимоги немає. Але в дочірньому інтерфейсі java.io.Closeable ідемпотентність є суворо обов’язковою специфікацією (повторний виклик close() зобов’язаний бути no-op). На практиці всі якісні реалізації роблять ідемпотентними.

  4. Що станеться, якщо передати в TWR змінну, значення якої потім змінюється в коді? Відповідь: Код не скомпілюється з помилкою local variables referenced from a try-with-resources statement must be final or effectively final. Змінна має бути незмінною, щоб унеможливити конкурентну підміну ресурсу під час виконання блоку try.

Червоні прапорці (чого категорично не можна говорити)

  • ❌ «Для try-with-resources підходить будь-який клас, що має метод close()» (клас зобов’язаний явно реалізувати інтерфейс AutoCloseable).
  • ❌ «Метод close() зобов’язаний викидати виняток, якщо ресурс уже був закритий раніше» (метод close() повинен бути ідемпотентним).
  • ❌ «У Java 9 в try-with-resources можна передавати змінні, які модифікуються в наступних рядках» (змінні зобов’язані бути strictly final або effectively final).
  • ❌ «try-with-resources завжди фізично закриває сокет бази даних» (у пулах з’єднань типу HikariCP метод close() повертає конекшн у пул).

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