Питання 9 · Розділ 7

Що таке try-with-resources?

Раніше потрібно було вручну закривати ресурси у finally:

Мовні версії: English Russian Ukrainian

Junior Level

Визначення

Try-with-resources (TWR) — це конструкція Java 7+. Мінімальна версія: Java 7. Для Java 6 і нижче — використовуйте finally. В Android — API 19+ (KitKat). Автоматично закриває ресурси після виходу з блоку try.

Навіщо потрібен

Раніше потрібно було вручну закривати ресурси у finally:

// Старий підхід (до Java 7)
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("file.txt"));
    br.readLine();
} finally {
    if (br != null) br.close(); // Вручну!
}

Тепер автоматично:

// Сучасний підхід (Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    br.readLine();
} // br.close() викличеться автоматично!

Які ресурси можна використовувати

Будь-які, що реалізують AutoCloseable:

  • FileInputStream, BufferedReader — файли
  • Connection, Statement — база даних
  • Socket — мережа

Кілька ресурсів

try (FileInputStream fis = new FileInputStream("in.txt");
     FileOutputStream fos = new FileOutputStream("out.txt")) {
    // Обидва закриються автоматично
}

Коли НЕ використовувати try-with-resources

  1. Фабричні методи – створюєте ресурс і повертаєте викликачому (TWR закриє його до return)
  2. Ресурс живе довше одного метода – lifecycle керується ззовні
  3. Пульовані ресурси (HikariCP connection) – повернення у пул != закриття, пул сам керує

Middle Level

Як це працює

Компілятор розгортає TWR у код з finally та обробкою suppressed винятків:

// Ваш код
try (Resource r = new Resource()) {
    r.doWork();
}

// Компілятор створює цей шаблон, щоб:
// (1) гарантувати закриття ресурсу
// (2) не втратити виняток з close() якщо в try вже була помилка
//     -- він додається у suppressed-список primaryException
// Розгортається у:
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 t) { primaryException.addSuppressed(t); }
        } else {
            r.close();
        }
    }
}

Suppressed Exceptions

Якщо у try впав виняток #1, а при close() — виняток #2:

  • #1 залишається основним
  • #2 додається як suppressed (придушений)
  • Ви отримуєте повну картину: Error A (Suppressed: Error B)

Отримати suppressed: exception.getSuppressed()

Ефект пастки вкладених ресурсів

// НЕБЕЗПЕЧНО — FileWriter може утекти!
try (BufferedWriter writer = new BufferedWriter(new FileWriter("file.txt"))) {
    // Якщо конструктор BufferedWriter впаде, FileWriter НЕ закриється
}

// БЕЗПЕЧНО — кожен ресурс окремо
try (FileWriter fw = new FileWriter("file.txt");
     BufferedWriter writer = new BufferedWriter(fw)) {
    writer.write("data");
}

Java 9+ — effectively final

Можна використовувати зовнішні змінні:

BufferedReader br = createReader();
try (br) { // br — effectively final
    br.readLine();
}

Senior Level

Порядок закриття (LIFO)

Ресурси закриваються у порядку, зворотному їх оголошенню:

LIFO (Last In, First Out) – останнім відкритим, першим закритим. Як стопка тарілок: останню поклав – першу береш. Для ресурсів це значить: най«внутрішніший» ресурс закривається першим.

try (Socket socket = new Socket("localhost", 8080);
     OutputStream os = socket.getOutputStream();
     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os))) {
    // Закриються: writer → os → socket
}

Це критично для стеків потоків. Якщо закрити Socket раніше Stream, Stream впаде при спробі flush.

Null Resources

Якщо ресурс у дужках ініціалізований як null, TWR просто проігнорує його — NPE не буде.

Продуктивність: Suppressed Overhead

При закритті 1000 ресурсів і масовому збої, об’єкт основного винятку містить величезний список suppressed. Це може вдарити по пам’яті.

Edge Cases

  • Catch/Finally порядок — ваші catch та finally виконуються після закриття ресурсів
  • Всередині catch ресурси вже недоступні (закриті)
  • Компілятор створює приховану локальну змінну для зовнішніх ресурсів (Java 9+)

Діагностика

  • Throwable.getSuppressed() — завжди перевіряйте цей масив при проблемах з інфраструктурою
  • Static Analysis — SonarLint знаходить ресурси, створені всередині try блоку, але не у дужках
  • javap -v — подивіться згенерований байт-код для розуміння механізму

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Try-with-resources (TWR) — Java 7+, автоматично закриває ресурси після виходу з try
  • Ресурс зобов’язаний реалізовувати AutoCloseable
  • Компілятор розгортає TWR у finally з обробкою suppressed винятків
  • Suppressed exceptions: якщо у try та close() впали два — #1 основний, #2 suppressed
  • Порядок закриття — LIFO (останнім відкритим, першим закритим)
  • Ефект пастки вкладених ресурсів: new B(new A()) — якщо B впаде, A утече
  • Java 9+ підтримує effectively final зовнішні змінні
  • Null ресурс у TWR просто ігнорується — NPE не буде

Часті уточнюючі запитання:

  • Чому вкладені ресурси небезпечні? — Якщо конструктор зовнішнього ресурсу впаде, внутрішній утече; оголошуйте кожен окремо
  • У якому порядку закриваються ресурси? — LIFO: останнім оголошеним закривається першим
  • Що буде якщо ресурс = null? — TWR проігнорує його, NPE не виникне
  • Коли НЕ використовувати TWR? — Фабричні методи (повернення з метода), пульовані ресурси, ресурс живе довше метода

Червоні прапорці (НЕ говорити):

  • “Я створюю ресурси всередині try, але не у дужках” — вони не закриються автоматично
  • “При масовому збої suppressed винятки не важливі” — вони можуть містити критичну діагностику
  • “TWR працює лише з InputStream” — працює з будь-яким AutoCloseable

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

  • [[Які вимоги до ресурсів в try-with-resources]]
  • [[Що таке інтерфейс AutoCloseable]]
  • [[В чому різниця між AutoCloseable та Closeable]]
  • [[Що таке suppressed exceptions]]
  • [[Чи завжди виконується блок finally]]