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

В чому різниця між AutoCloseable та Closeable

Головна різниця полягає в ієрархії, пакеті, типі винятку, що викидається, та сфері застосування: 4. Призначення: AutoCloseable (Java 7) є універсальним для будь-яких ресурсів (J...


🟢 Junior Level

Коротка відповідь для співбесіди (30 секунд)

Головна різниця полягає в ієрархії, пакеті, типі винятку, що викидається, та сфері застосування:

  1. Ієрархія: Closeable є прямим нащадком AutoCloseable (Closeable extends AutoCloseable).
  2. Пакет: AutoCloseable розташований у java.lang (базовий API ядра), а Closeable — у java.io (потоки введення-виведення).
  3. Виняток: метод AutoCloseable.close() оголошує throws Exception, тоді як Closeable.close() звужує його до throws IOException.
  4. Призначення: AutoCloseable (Java 7) є універсальним для будь-яких ресурсів (JDBC-з’єднання, транзакції, мережеві сокети, пули потоків), а Closeable (Java 5) історично проєктувався суто для потоків введення-виведення байтів і символів.

Обидва інтерфейси повністю підтримуються синтаксичною конструкцією try-with-resources (TWR).


Порівняльна таблиця

Критерій java.lang.AutoCloseable java.io.Closeable
Версія Java З’явився в Java 7 З’явився в Java 5 (розширений у Java 7)
Пакет java.lang (не потребує import) java.io (потребує явного імпорту)
Сигнатура close() void close() throws Exception; void close() throws IOException;
Ієрархія Базовий інтерфейс верхнього рівня Дочірній інтерфейс (extends AutoCloseable)
Ідемпотентність Рекомендується (strongly recommended) Суворо обов’язкова (mandatory contract)
Сфера застосування Будь-які ресурси (БД, транзакції, пули, замки) Лише структури введення-виведення I/O (файли, стріми, сокети)

Ієрархія успадкування

java.lang.AutoCloseable (Java 7)
    │   void close() throws Exception;
    │
    ├── java.sql.Connection / Statement / ResultSet
    ├── java.util.concurrent.ExecutorService (Java 19+)
    │
    └── java.io.Closeable (Java 5, extends AutoCloseable з Java 7)
            void close() throws IOException;
            │
            ├── java.io.InputStream / OutputStream
            ├── java.io.Reader / Writer
            └── java.nio.channels.Channel

Приклад використання обох інтерфейсів у TWR

Обидва типи ресурсів є взаємозамінними в круглих дужках try-with-resources:

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ResourceComparisonExample {
    public static void runDemo() {
        // FileInputStream реалізує Closeable (I/O)
        // Connection реалізує AutoCloseable (JDBC)
        try (FileInputStream fis = new FileInputStream("app.properties");
             Connection conn = DriverManager.getConnection("jdbc:h2:mem:test")) {
            
            int data = fis.read();
            System.out.println("First byte: " + data);
            System.out.println("Connection valid: " + conn.isValid(2));
            
        } catch (IOException | SQLException e) {
            System.err.println("Resource handling error: " + e.getMessage());
        }
    }
}

🟡 Middle Level

Архітектура зворотної сумісності

Коли в Java 7 проєктувався механізм try-with-resources (Project Coin / JSR 334), виникла потреба автоматично закривати не лише файли та сокети (java.io), а й підключення до баз даних (java.sql), канали (java.nio) та будь-які прикладні ресурси.

Якби розробники прив’язали оператор try-with-resources до наявного java.io.Closeable:

  1. Виникла б небажана сильна зв’язність між модулями ядра/баз даних і пакетом java.io.
  2. Уся підсистема JDBC була б змушена викидати IOException під час закриття з’єднань або залежати від контрактів введення-виведення.

Тому було створено більш загальний кореневий інтерфейс java.lang.AutoCloseable, а чинний java.io.Closeable оголосили його спадкоємцем:

package java.io;

public interface Closeable extends AutoCloseable {
    @Override
    void close() throws IOException;
}

Завдяки коваріантності сигнатур методів у Java (метод підтипу має право декларувати підмножину перевірюваних винятків базового методу), Closeable коректно звузив throws Exception до throws IOException. При цьому мільйони рядків чинного коду Java 5 та 6 автоматично отримали сумісність із новим синтаксисом TWR без перекомпіляції.


Тонкощі контракту: Ідемпотентність виклику close()

Специфікація Javadoc формулює вимогу щодо ідемпотентності для двох інтерфейсів з різним ступенем категоричності:

  1. Closeable (Сувора вимога):

    “If the stream is already closed then invoking this method has no effect.” Повторний виклик методу close() у будь-якого класу, що реалізує Closeable, не повинен виконувати жодних дій (no-op) і не має права викидати винятки.

  2. AutoCloseable (Наполеглива рекомендація):

    “Note that unlike the close method of Closeable, this close method is not required to be idempotent. In other words, calling this close method more than once may have some visible side effect… However, implementers of this interface are strongly recommended to make their close methods idempotent.”

Чому AutoCloseable не вимагає ідемпотентності на 100%?

Деякі системні ресурси мають складну транзакційну природу. Наприклад:

  • Транзакційний ресурс, у якого close() виконує неявний коміт або відкат (rollback). Повторна спроба завершити вже закриту транзакцію може свідчити про порушення логічної цілісності даних, тому ресурс має право викинути IllegalStateException.
  • Розподілені блокування або стейт-машини.

Проте у 99% прикладних класів метод close() слід робити ідемпотентним, щоб уникнути прихованих помилок при закритті ланцюжків декораторів.


Зв’язок із Flushable

Переважна більшість реалізацій Closeable у java.io одночасно реалізують інтерфейс java.io.Flushable:

public class BufferedOutputStream extends FilterOutputStream implements Flushable, Closeable {
    @Override
    public void close() throws IOException {
        try {
            flush(); // 1. Скидання накопиченого буфера на диск або в мережу
        } finally {
            super.close(); // 2. Гарантоване закриття нативного дескриптора
        }
    }
}

Якщо під час виклику flush() виникає мережевий збій або закінчується місце на диску, викидається IOException. Якби flush() не було огорнуто в try-finally, сам нативний дескриптор файлу ризикував би залишитися незакритим! Якісні реалізації в JDK завжди використовують try-finally всередині close().


🔴 Senior Level

Ідіоматичний вибір: що реалізовувати у власному коді?

Потрібно закривати ресурс?
         │
         ├── Це потік байтів, символів, сокет або канал I/O?
         │         └── ТАК ──> Реалізуйте java.io.Closeable (throws IOException)
         │
         └── Це бізнес-сервіс, транзакція, контекст або ThreadLocal?
                   └── ТАК ──> Реалізуйте java.lang.AutoCloseable
                                 │
                                 ├── Чи може закриття кинути перевірюваний виняток?
                                 │         ├── ТАК ──> Звузьте до конкретного CheckedException
                                 │         └── НІ  ──> Приберіть throws взагалі (void close())

Патерн “No-Throws AutoCloseable” для Scope Guard

Для тимчасової зміни контексту (наприклад, MDC логера, Security Context, підміни назви потоку) створюють ресурс без перевірюваних винятків. Це звільняє викликаючий код від необхідності писати фіктивні блоки catch:

import org.slf4j.MDC;

public final class MdcScope implements AutoCloseable {
    private final String previousValue;
    private final String key;

    public MdcScope(String key, String value) {
        this.key = key;
        this.previousValue = MDC.get(key);
        MDC.put(key, value);
    }

    // Сигнатура БЕЗ throws: чистий try-with-resources без блоку catch
    @Override
    public void close() {
        if (previousValue != null) {
            MDC.put(key, previousValue);
        } else {
            MDC.remove(key);
        }
    }
}

// Використання:
public void processOrder(String orderId) {
    try (var ignored = new MdcScope("orderId", orderId)) {
        // Уся логіка всередині автоматично логується з [orderId=12345]
        executeBusinessLogic();
    } // close() викликається гарантовано, перехоплення винятків не потрібне!
}

Декоратори, каскадне закриття та Resource Ownership

Патерн «Декоратор» у java.io (наприклад, BufferedReader(InputStreamReader(FileInputStream))) прив’язує володіння ресурсами. Закриття зовнішнього декоратора спричиняє каскадний виклик close() на всіх вкладених компонентах.

Проблема закриття спільних ресурсів (Shared Resource Problem)

Якщо в декоратор передається потік, що не належить поточному методу (наприклад, System.in або сокет із пулу), закриття декоратора закриє спільний ресурс для всього застосунку:

public void processConsoleInput() {
    // ❌ ПОМИЛКА: Scanner.close() закриє System.in для всієї JVM!
    try (Scanner scanner = new Scanner(System.in)) {
        String input = scanner.nextLine();
    }
    // Усі наступні спроби читання з System.in призведуть до IOException: Stream Closed!
}

Рішення: “Uncloseable” обгортка

Якщо потік не повинен закриватися декоратором, використовують спеціальний проксі або делегат:

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

public class NonClosingInputStream extends FilterInputStream {
    public NonClosingInputStream(InputStream in) {
        super(in);
    }

    @Override
    public void close() throws IOException {
        // no-op: запобігаємо закриттю системного потоку
    }
}

Non-blocking I/O та закриття потоків у стані очікування

У пакеті java.nio.channels інтерфейс Channel розширює Closeable. Якщо один потік заблокований в операції читання/запису на каналі (read(), write()), а інший потік викликає channel.close():

  1. Заблокований потік негайно розблокується та отримає виняток java.nio.channels.AsynchronousCloseException.
  2. Якщо заблокований потік був перерваний (Thread.interrupt()), канал буде негайно закритий, а потік отримає java.nio.channels.ClosedByInterruptException.

У старому блокуючому java.io (сокети InputStream.read()) закриття стріму з іншого потоку не завжди гарантувало миттєве розблокування на рівні ОС до появи каналів NIO.


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

Питання для швидкої перевірки

  1. Чому Closeable успадковує AutoCloseable, а не навпаки? Відповідь: AutoCloseable з’явився пізніше (у Java 7) як більш загальна абстракція ядра (java.lang) для будь-яких ресурсів із широким контрактом throws Exception. Успадкування Closeable extends AutoCloseable дозволило всім наявним I/O-класам із Java 5 миттєво отримати сумісність із try-with-resources без зламу чинного API.
  2. Чому в Closeable ідемпотентність методу close() суворо обов’язкова, а в AutoCloseable лише рекомендована? Відповідь: Для потоків введення-виведення повторний close() однозначно безпечний і повинен ігноруватися. Для довільних ресурсів (транзакції, розподілені блокування, стейт-машини) повторне закриття може свідчити про порушення логіки програми та вимагати явного викидання винятку (наприклад, IllegalStateException).
  3. Що станеться, якщо в класі, що реалізує AutoCloseable, метод close() викине неперевірюваний виняток (RuntimeException)? Відповідь: Метод завершиться аварійно. Якщо блок try виконувався без помилок, цей RuntimeException полетить далі з блоку TWR. Якщо ж у тілі try вже виник первинний виняток, RuntimeException із close() буде пригнічено і додано до основного через primaryException.addSuppressed(e).
  4. Який інтерфейс слід реалізувати для класу, що керує транзакцією бази даних? Відповідь: java.lang.AutoCloseable. При реалізації слід звузити виняток до SQLException (чи власного доменного винятку) або прибрати throws, якщо метод виконує тихий rollback незафіксованої транзакції.

Типові помилки та Red Flags

  • ❌ Твердження, що Closeable кидає RuntimeException: Closeable викидає суворо IOException (перевірюваний виняток).
  • ❌ Твердження, що AutoCloseable призначений лише для введення-виведення (I/O): Це груба помилка; AutoCloseable знаходиться в java.lang і призначений для будь-яких прикладних ресурсів.
  • ❌ Закриття системних потоків (System.in, System.out) через обгортки Closeable: Призводить до глобальної недоступності стандартних консольних потоків у процесі JVM.
  • ❌ Залишення throws Exception у власному класі без реальної потреби: Змушує викликаючий код писати неспецифічні обробники catch (Exception e).

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