Конструкция try-with-resources в Java
В классическом try-finally есть неочевидный дефект: если br.readLine() выбросил IOException, а затем br.close() в блоке finally тоже упал, наружу уйдёт только исключение из close() — настоящая причина сбоя бесследно теряется. Конструкция try-with-resources решает и эту проблему, и проблему забытого вызова close().
try-with-resources — это форма оператора try (появилась в Java 7), в которой ресурсы объявляются в круглых скобках после try и закрываются автоматически: метод close() вызывается при выходе из блока try — и при обычном завершении, и при исключении. Ресурсом может быть любой объект, класс которого реализует интерфейс java.lang.AutoCloseable (в том числе его наследник java.io.Closeable): потоки ввода-вывода, сокеты, JDBC-соединения, Scanner.
Синтаксис try-with-resources
Ресурс объявляется и инициализируется в круглых скобках сразу после ключевого слова try; несколько ресурсов разделяются точкой с запятой. Блоки catch и finally необязательны: если исключения объявлены в throws метода, достаточно одного блока try.
try (ResourceType1 name1 = new ResourceType1(...);
ResourceType2 name2 = new ResourceType2(...)) {
// работа с ресурсами
} catch (IOException e) {
// необязательно
} finally {
// необязательно
} Переменная ресурса неявно final и видна только внутри блока try: в catch и finally она уже недоступна, а ресурс к этому моменту закрыт.
try-finally и try-with-resources: сравнение
До Java 7 ресурс закрывали вручную в блоке finally; try-with-resources делает то же самое автоматически, короче и надёжнее: не нужно писать вызов close(), а исключение из close() не затирает исходное. Сравним два варианта метода, читающего первую строку файла.
Пример 1. Закрытие ресурса в блоке finally
public static String readFirstLineWithFinally(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
} Проверка br != null здесь не нужна: если файла нет, new FileReader(path) выбросит FileNotFoundException ещё до входа в блок try, и переменная просто не будет создана.
Перепишем этот метод с помощью try-with-resources.
Пример 2. Тот же метод с try-with-resources
public static String readFirstLine(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
// Файл содержит строки "Hello" и "World":
// readFirstLine("a.txt") -> "Hello"
// readFirstLine("missing.txt") -> java.io.FileNotFoundException | Критерий | try-finally | try-with-resources |
|---|---|---|
| Закрытие ресурса | Вручную, вызовом close() в finally | Автоматически при выходе из блока try |
Исключение из close() | Заменяет исходное исключение из try — причина сбоя теряется | Добавляется к исходному как подавленное (getSuppressed()) |
| Несколько ресурсов | Вложенные try-finally для каждого ресурса | Одни скобки, ресурсы через ;, закрытие в обратном порядке |
| Требования к ресурсу | Любой объект с методом закрытия | Класс должен реализовывать AutoCloseable |
| Версия Java | Любая | Java 7+; ресурс-переменная из внешнего кода — Java 9+ |
Несколько ресурсов и порядок закрытия
В одном try-with-resources можно объявить несколько ресурсов через точку с запятой; они открываются слева направо, а закрываются в обратном порядке — последний открытый закрывается первым. Если конструктор второго ресурса выбросит исключение, уже открытый первый ресурс всё равно будет закрыт.
Пример 3. Два ресурса в одном блоке try
public static String readFirstLine2(String path) throws IOException {
try (FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr)) {
return br.readLine();
}
} Сначала закроется br, затем fr. BufferedReader.close() сам закрывает вложенный FileReader, поэтому второй вызов close() для fr ничего не делает — это безопасно, потому что повторное закрытие Closeable не должно давать эффекта.
Порядок хорошо виден на собственном ресурсе, который печатает сообщения при открытии и закрытии.
Пример 4. Порядок открытия и закрытия ресурсов
public class CloseOrderDemo {
static class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
System.out.println("Открыт " + name);
}
@Override
public void close() {
System.out.println("Закрыт " + name);
}
}
public static void main(String[] args) {
try (Resource a = new Resource("A");
Resource b = new Resource("B")) {
System.out.println("Работа с ресурсами");
}
}
}
// Вывод:
// Открыт A
// Открыт B
// Работа с ресурсами
// Закрыт B
// Закрыт A Обратите внимание: метод close() здесь объявлен без throws Exception, поэтому блок catch не нужен. Если бы close() бросал проверяемое исключение, компилятор потребовал бы его обработать или объявить в throws.
AutoCloseable и Closeable: что может быть ресурсом
Ресурсом в try-with-resources может быть только объект, класс которого реализует java.lang.AutoCloseable. Интерфейс java.io.Closeable расширяет AutoCloseable, поэтому все потоки ввода-вывода из java.io подходят автоматически. Разница между интерфейсами — в сигнатуре close() и требованиях к ней.
| Интерфейс | Пакет и версия | Сигнатура close() | Повторный вызов close() |
|---|---|---|---|
AutoCloseable | java.lang, Java 7 | void close() throws Exception | Может иметь побочные эффекты |
Closeable | java.io, Java 5; с Java 7 наследует AutoCloseable | void close() throws IOException | Должен ничего не делать (идемпотентность) |
При реализации собственного ресурса сужайте исключение в close(): объявите throws IOException или вообще не объявляйте проверяемых исключений, как в примере 4. Тогда вызывающему коду не придётся ловить общий Exception.
Подавленные исключения и getSuppressed()
Если исключение выброшено в блоке try, а затем close() тоже выбрасывает исключение, наружу уходит исключение из блока try, а исключение из close() прикрепляется к нему методом addSuppressed(). Получить подавленные исключения можно методом Throwable.getSuppressed(), который возвращает массив Throwable[].
Пример 5. Что выведет этот код?
public class SuppressedDemo {
static class FaultyResource implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("ошибка в close()");
}
}
public static void main(String[] args) {
// Вариант 1: try-finally
try {
FaultyResource r = new FaultyResource();
try {
throw new RuntimeException("ошибка в блоке try");
} finally {
r.close();
}
} catch (RuntimeException e) {
System.out.println("finally: " + e.getMessage()
+ ", подавленных: " + e.getSuppressed().length);
}
// Вариант 2: try-with-resources
try (FaultyResource r = new FaultyResource()) {
throw new RuntimeException("ошибка в блоке try");
} catch (RuntimeException e) {
System.out.println("try-with-resources: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println(" подавлено: " + s.getMessage());
}
}
}
}
// Вывод:
// finally: ошибка в close(), подавленных: 0
// try-with-resources: ошибка в блоке try
// подавлено: ошибка в close() В варианте с finally исходное исключение «ошибка в блоке try» потеряно полностью — именно поэтому отладка такого кода превращается в поиск ошибки, которой «нет». Try-with-resources сохраняет обе ошибки.
Где искать подавленное исключение
Отдельно в лог подавленные исключения не попадают, но e.printStackTrace() и большинство логгеров выводят их внутри стек-трейса основного исключения строкой Suppressed: .... Если в логе видна только «верхняя» ошибка, прокрутите стек-трейс ниже.
catch и finally в try-with-resources
Блоки catch и finally в try-with-resources выполняются уже после того, как все ресурсы закрыты. Поэтому в catch нельзя «спасти» данные через ресурс, а исключение, выброшенное при закрытии, тоже можно поймать в этом catch.
Пример 6. Порядок выполнения
// Resource — класс из примера 4
try (Resource r = new Resource("R")) {
throw new IllegalArgumentException("сбой");
} catch (IllegalArgumentException e) {
System.out.println("catch: " + e.getMessage());
} finally {
System.out.println("finally");
}
// Вывод:
// Открыт R
// Закрыт R
// catch: сбой
// finally Если исключение выброшено при создании ресурса (например, FileNotFoundException в конструкторе FileReader), тело блока try не выполняется, закрывать нечего, и управление сразу переходит в подходящий catch.
Java 9: ресурс, объявленный до try
Начиная с Java 9 в скобках try-with-resources можно указать уже существующую переменную, если она final или effectively final (после инициализации ей ничего не присваивается). В Java 7 и 8 ресурс нужно было обязательно объявлять внутри скобок, даже если объект уже был создан.
// Java 7/8: приходилось заводить новую переменную
public static String readFirstLine(BufferedReader reader) throws IOException {
try (BufferedReader br = reader) {
return br.readLine();
}
}
// Java 9+: достаточно указать переменную
public static String readFirstLine9(BufferedReader reader) throws IOException {
try (reader) {
return reader.readLine();
}
} Если переменной где-либо в методе присваивается новое значение, компиляция try (reader) завершится ошибкой: переменная перестаёт быть effectively final.
На чём чаще всего ошибаются
Типичные промахи с try-with-resources связаны с областью видимости ресурса, обёртками над потоками и ресурсами, которые закрывать не нужно. Ниже — что происходит в каждом случае и как исправить.
| Ошибка | Что происходит | Как правильно |
|---|---|---|
Присвоить ресурсу новое значение внутри try: br = new BufferedReader(...) | Ошибка компиляции: auto-closeable resource br may not be assigned | Объявить отдельную переменную или отдельный try-with-resources |
Обратиться к ресурсу в catch или finally | Ошибка компиляции cannot find symbol: переменная вне области видимости | Нужные данные сохранить в переменную, объявленную до try |
Обёртка в одном объявлении: new ObjectInputStream(new FileInputStream(f)) | Конструктор ObjectInputStream читает заголовок и может выбросить StreamCorruptedException; внутренний FileInputStream не был объявлен ресурсом и остаётся открытым | Объявить каждый поток отдельным ресурсом: try (FileInputStream in = new FileInputStream(f); ObjectInputStream ois = new ObjectInputStream(in)) |
Files.lines(path) или Files.list(dir) без try-with-resources | Stream держит открытый дескриптор файла до сборки мусора; при массовой обработке — «Too many open files» | try (Stream<String> lines = Files.lines(path)) { ... } |
Объявить ресурсом new Scanner(System.in) | Закрывается System.in; следующее чтение с консоли выбросит NoSuchElementException | Стандартный ввод не закрывать; подробнее о закрытии Scanner — в уроке Класс Scanner |
| Считать, что try-with-resources ловит исключения | Он только закрывает ресурсы; проверяемое IOException без catch или throws не скомпилируется | Добавить catch или объявить исключение в throws |
Примеры чтения и записи файлов через FileReader, BufferedWriter и PrintWriter в try-with-resources разобраны в уроке Классы символьных потоков.
Часто задаваемые вопросы
Есть ли try-with-resources в Kotlin?
Отдельной конструкции в Kotlin нет — её заменяет функция-расширение use { } для Closeable и AutoCloseable: BufferedReader(FileReader(path)).use { it.readLine() }. Она вызывает close() после выполнения лямбды, в том числе при исключении, и добавляет исключение из close() к основному как подавленное. Для нескольких ресурсов вызовы use вкладывают друг в друга.
Что будет, если ресурс равен null?
Ничего страшного: try (BufferedReader br = null) { } компилируется и выполняется без NullPointerException. По спецификации языка для ресурса со значением null метод close() просто не вызывается. NPE возникнет только при обращении к такому ресурсу внутри блока try.
Нужно ли вызывать flush() перед закрытием writer в try-with-resources?
Нет. Метод close() у BufferedWriter, PrintWriter, BufferedOutputStream сначала сбрасывает буфер, а затем закрывает поток. flush() нужен, только если данные должны попасть в файл до закрытия — например, когда этот же файл сразу читается другим потоком внутри того же блока try.
Что выполнится раньше: close(), catch или finally?
Сначала закрываются все ресурсы (в обратном порядке объявления), затем выполняется catch, последним — finally. Этот вопрос часто задают на собеседованиях; пример с выводом — в разделе catch и finally в try-with-resources.
Можно ли использовать try-with-resources с JDBC?
Да, и это рекомендуемый способ: Connection, Statement, PreparedStatement и ResultSet реализуют AutoCloseable. Обычно их объявляют в одних скобках — try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)), и они закрываются в обратном порядке: сначала запрос, потом соединение.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
- Класс FileCopy
- Класс FileInputOutputStreamDemo
- Копирование файла в другой файл
- Запись отчёта о сотрудниках в файл
- Запись чека в файл через PrintWriter
Комментарии