Що таке 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:
- Якщо в блоці
tryвиниклоSQLException(основна причина аварії). - А під час автоматичного закриття
close()виниклоIOException. - Вихідне
SQLExceptionлетить далі як головне, аIOExceptionприкріплюється до нього у список пригнічених (Suppressed). - Отримати доступ до них можна за допомогою виклику
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.Головні особливості:
- LIFO-порядок закриття: Ресурси закриваються у зворотному порядку (останній відкритий закривається першим).
- Suppressed Exceptions: Виняток із методу
close()не затирає первинний виняток із тілаtry, а додається до нього як пригнічений (addSuppressed()).- Вкладені ресурси: Обгортки потоків (
new BufferedWriter(new FileWriter())) потрібно оголошувати окремо через крапку з комою, інакше в разі збою зовнішнього конструктора внутрішній ресурс безповоротно витече.- У Java 9+ дозволено передавати
effectively finalзмінні:try (existingResource).
4 каверзних питання з відповідями
-
У якому порядку викликаються методи
close(), якщо в заголовку try оголошено 3 ресурси? Відповідь: Суворо у зворотному порядку (LIFO — Last In, First Out). Ресурс, оголошений третім, закриється першим; ресурс, оголошений першим, закриється останнім. -
Що станеться, якщо змінна ресурсу в круглих дужках try-with-resources дорівнює null? Відповідь: Жодної помилки не виникне,
NullPointerExceptionне буде викинуто. Байткод, сгенерований компілятором, містить попередню перевіркуif (resource != null)перед викликом методуclose(). -
Чи будуть ресурси вже закритими в момент входу в явний блок catch? Відповідь: Так. Автоматичне закриття всіх ресурсів (виклик
AutoCloseable.close()) відбувається відразу після виходу з блокуtry, суворо до передачі керування в явні блокиcatchтаfinally. -
Чим небезпечна конструкція
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).