Введение

В Java нелегко управлять ресурсами вроде дескрипторов файлов, подключений к базам данных и сетевых сокетов. Для корректного закрытия ресурсов, операции над которыми завершены, разработчиками традиционно применялись блоки try-finally. Но этот подход подвержен ошибкам и чреват утечкой ресурсов, если реализован некорректно. Представленный в Java 7 оператор try-with-resources надежнее и эффективнее для управления ресурсами. Изучим синтаксис try-with-resources, его преимущества по сравнению с традиционными блоками try-finally и приведем практические примеры управления различными ресурсами.

Синтаксис try-with-resources

Оператор try-with-resources в Java  —  это специализированная разновидность традиционного блока try, предназначенная для упрощения управления ресурсами, которые необходимо закрывать после использования. Прежде чем переходить к синтаксису, разберемся с ресурсами: какие доступны оператору try-with-resources и как они обрабатываются компилятором Java.

Что такое «ресурс»?

В Java ресурс  —  это любой объект, которым реализуется интерфейс AutoCloseable, куда включается и интерфейс Closeable. Часто ресурсы  —  это внешние или системного уровня сущности, которые необходимо явным образом высвободить, если они больше не используются. Типичные примеры: файловые потоки, подключения баз данных, сетевые сокеты и другие объекты, связанные с вводом-выводом.

Интерфейсом AutoCloseable определяется единственный метод:

public interface AutoCloseable {
void close() throws Exception;
}

Любой класс, которым реализуется этот интерфейс, используется в блоке try-with-resources. В нем используются и классы, которыми реализуется старый интерфейс Closeable. Разработчикам, уже знакомым с Closeable, не сложно освоить этот новый функционал управления ресурсами. Closeable  —  это субинтерфейс AutoCloseable, им определяется метод close() с более специфичным исключением IOException:

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

Базовый синтаксис try-with-resources

Базовая структура оператора try-with-resources аналогична традиционному блоку try, но с важным отличием: ресурс объявляется и инициализируется в скобках сразу после ключевого слова try:

try (ResourceType resource = new ResourceType()) {
// Код, в котором используется ресурс
} catch (ExceptionType e) {
// Код обработки исключения
}

Разберем компоненты:

  • Объявление ресурса. Ресурс объявляется и инициализируется в скобках после ключевого слова try. Это объявление представляется обычно в виде объявления переменной, где ResourceType  —  это тип класса, а resource  —  имя переменной.
  • Использование ресурса. Внутри блока try ресурс используется при необходимости выполнения операций: считывание файла, запрос к базе данных, отправка данных по сетевому сокету.
  • Автоматическое закрытие ресурса. При завершении выполнения блока try  —  в обычном режиме или из-за исключения  —  для корректного высвобождения ресурса автоматически вызывается его метод close().

Обработка нескольких ресурсов

Одна из суперспособностей try-with-resources  —  управление в одном операторе несколькими ресурсами. Чтобы поработать с несколькими ресурсами одновременно, их всех объявляют в одном блоке try, разделяя точкой с запятой:

try (BufferedReader br = new BufferedReader(new FileReader("file1.txt"));
BufferedReader br2 = new BufferedReader(new FileReader("file2.txt"))) {

String line1, line2;
while ((line1 = br.readLine()) != null && (line2 = br2.readLine()) != null) {
System.out.println("File1: " + line1);
System.out.println("File2: " + line2);
}
} catch (IOException e) {
e.printStackTrace();
}

В этом примере оба экземпляра BufferedReader объявлены в операторе try. В Java оба ресурса закрываются в порядке, обратном порядку их создания  —  сначала br2, затем br  —  независимо от того, завершается ли блок try в обычном режиме или из-за исключения.

Обработка исключений и подавленные исключения

Обработка исключений  —  важный аспект оператора try-with-resources. Если в блоке try выбрасывается исключение и при закрытии ресурса  —  еще одно, второе исключение в Java не удаляется, а добавляется к первому как «подавленное исключение». Так не теряется важная информация о том, что пошло не так при управлении ресурсами.

Рассмотрим такой сценарий:

try (CustomResource resource = new CustomResource()) {
throw new Exception("Exception during processing");
} catch (Exception e) {
System.err.println("Caught: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("Suppressed: " + suppressed.getMessage());
}
}

Если методом close() в CustomResource выбрасывается исключение, оно добавляется в список подавленных основного исключения, выброшенного в блоке try. В выходные данные включаются сведения как об исходном, так и о подавленном исключениях с полной информацией о том, что пошло не так.

Совместимость с имеющимся кодом

Другая суперспособность try-with-resources  —  она легко интегрируется с имеющимися кодовыми базами. Любой класс, которым реализуется интерфейс AutoCloseable или Closeable, непосредственно и без изменений используется в операторе try-with-resources. Такой обратной совместимостью обеспечивается доступность для устаревших ресурсов преимуществ современных, безопасных техник управления ресурсами, причем без существенного рефакторинга.

Преимущества try-with-resources перед традиционными блоками try-finally

Оператор try-with-resources  —  это шаг вперед по сравнению с традиционными блоками try-finally для управления ресурсами. Хотя подход try-finally по-прежнему актуален, с try-with-resources ресурсы обрабатываются элегантнее, безопаснее, безошибочнее. Изучим преимущества try-with-resources в сравнении с try-finally по таким аспектам, как автоматическое управление ресурсами, чистый код, стереотипный код, обработка исключений и удобство восприятия кода.

Автоматическое управление ресурсами

Это самое существенное преимущество try-with-resources. В традиционных блоках try-finally разработчиками явным образом закрываются ресурсы внутри finally. Такой подход подвержен человеческим ошибкам, особенно в сложном коде со множеством ресурсов. Если разработчик забывает закрыть ресурс, это чревато утечкой ресурсов, что сказывается снижением производительности приложения или приводит к его аварийному завершению.

Рассмотрим сценарий управления ресурсами FileWriter и PrintWriter:

FileWriter fw = null;
PrintWriter pw = null;
try {
fw = new FileWriter("output.txt");
pw = new PrintWriter(fw);
pw.println("Hello, world!");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (pw != null) {
pw.close();
}
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Этому блоку кода требуется ручное закрытие FileWriter и PrintWriter в блоке finally, что чревато не только ошибками, но и загромождением кода. Например, если забыть закрыть pw, файл очищается некорректно, что чревато неполным записыванием данных.

С try-with-resources эта сложность устраняется:

try (FileWriter fw = new FileWriter("output.txt");
PrintWriter pw = new PrintWriter(fw)) {
pw.println("Hello, world!");
} catch (IOException e) {
e.printStackTrace();
}

Здесь FileWriter и PrintWriter автоматически закрываются при выходе из блока try независимо от того, как осуществляется этот выход. Такой автоматизацией значительно снижается риск утечки ресурсов, упрощается управление ими.

Код чище, стереотипного кода меньше

Одна из основных целей оператора try-with-resources  —  сокращение стереотипного кода, который приходится писать разработчикам. В традиционном блоке try-finally раздел finally иногда длиннее самого кода использования ресурсов, из-за чего сложнее отследить логику и выше вероятность ошибок.

Рассмотрим сценарий, в котором нужно открыть подключение к файлу, записать данные, а затем закрыть подключение. Вот код с традиционным блоком try-finally:

FileOutputStream fos = null;
try {
fos = new FileOutputStream("data.bin");
fos.write(new byte[]{1, 2, 3, 4, 5});
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Этот подход рабочий, но здесь много стереотипного кода. Особенно в блоке finally, где нужно снова обработать потенциальное IOException.

А вот с try-with-resources блок finally устраняется полностью:

try (FileOutputStream fos = new FileOutputStream("data.bin")) {
fos.write(new byte[]{1, 2, 3, 4, 5});
} catch (IOException e) {
e.printStackTrace();
}

Код получается чище, удобнее для восприятия, проще в сопровождении. Основная логика  —  запись данных в файл  —  не скрыта за кодом управления ресурсами, благодаря чему суть программы становится яснее.

Усовершенствована обработка исключений

Другое существенное преимущество try-with-resources  —  больше возможностей обработки исключений. В традиционном блоке try-finally, если исключение выбрасывается в блоках try и finally, последним скрывается исходное исключение. Отладка затрудняется, так как теряется первопричина проблемы.

Например, рассмотрим такой код:

FileInputStream fis = null;
try {
fis = new FileInputStream("input.txt");
// Обработка, при которой выбрасывается исключение
throw new RuntimeException("Processing exception");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Если при закрытии fis появляется исключение IOException, им маскируется выбрасываемое во время обработки исключение RuntimeException, из-за чего диагностировать проблему сложнее.

С try-with-resources оба исключения сохраняются. Исходное исключение распространяется, а исключение, которое появляется при закрытии ресурса, добавляется как подавленное. Так сохраняется вся необходимая информация:

try (FileInputStream fis = new FileInputStream("input.txt")) {
// Обработка, при которой выбрасывается исключение
throw new RuntimeException("Processing exception");
} catch (IOException e) {
e.printStackTrace();
} catch (RuntimeException e) {
System.err.println("Caught: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("Suppressed: " + suppressed.getMessage());
}
}

В этом случае RuntimeException  —  основное исключение, и любое IOException, которое появляется при вызове close(), перечисляется как подавленное. Этим значительно упрощаются отладка и анализ ошибок.

Удобнее для восприятия и сопровождения

Удобство восприятия и сопровождаемость кода  —  решающие факторы программной разработки, особенно в крупных проектах с большим количеством разработчиков. Удобство восприятия повышается оператором try-with-resources благодаря акцентированию логики использования ресурсов и устранению лишнего стереотипного кода.

Рассмотрим сценарий, в котором нужно выполнить операции с файлом, каждой из которых требуется аккуратное управление ресурсами. С традиционными блоками try-finally код усложняется, его трудно отследить, особенно когда увеличивается количество ресурсов:

RandomAccessFile raf = null;
try {
raf = new RandomAccessFile("file.dat", "rw");
raf.writeInt(100);
raf.seek(0);
System.out.println(raf.readInt());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Такой подход функционален, но не очень элегантен. Блоком finally добавляется шум, который отвлекает от основных операций, выполняемых с файлом.

А вот с try-with-resources тот же код пишется лаконичнее:

try (RandomAccessFile raf = new RandomAccessFile("file.dat", "rw")) {
raf.writeInt(100);
raf.seek(0);
System.out.println(raf.readInt());
} catch (IOException e) {
e.printStackTrace();
}

Управление ресурсами осуществляется автоматически, причем основная логика записи и считывания целого числа из файла сохраняется на переднем плане. Благодаря такой ясности упрощаются чтение и сопровождение кода, что особенно ценно в командной среде, где разработчикам необходимо понимать и менять кодовую базу.

Практические примеры: файловый ввод-вывод, подключения баз данных, сетевые ресурсы

Изучим практические примеры применения оператора try-with-resources в реальных сценариях и управление ресурсами: файловым вводом-выводом, подключениями баз данных, сетевыми ресурсами. В каждом примере продемонстрируем уникальные преимущества try-with-resources в обеспечении эффективной и безопасной работы с ресурсами, в частности гибкость try-with-resources в различных контекстах.

Файловый ввод-вывод: запись во временный файл

Файловый ввод-вывод  —  один из типичнейших сценариев try-with-resources. Как этим оператором упрощается считывание файла, мы уже видели. Рассмотрим пример, в котором данные записываются во временный файл. Во временных файлах часто сохраняются промежуточные данные, которые не нужно хранить длительно. Чтобы предотвратить загромождение кода и утечки ресурсов, эти файлы корректно закрывают и удаляют после использования.

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class TemporaryFileExample {
public static void main(String[] args) {
File tempFile = null;
try {
tempFile = File.createTempFile("tempfile", ".txt");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) {
writer.write("This is a temporary file.");
writer.newLine();
writer.write("It will be deleted after use.");
}

System.out.println("Temporary file written: " + tempFile.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
System.out.println("Temporary file deleted: " + tempFile.getAbsolutePath());
}
}
}
}

В этом примере оператором try-with-resources управляется BufferedWriter, которым данные записываются во временный файл. В блоке finally временный файл после использования удаляется, даже если в процессе записи файла появляется исключение. Благодаря такому подходу временный файл не задерживается в файловой системе, ресурсы высвобождаются, потенциальные конфликты предотвращаются.

Подключения баз данных: управление транзакциями с откатом

Подключения баз данных  —  другой важный ресурс, которому требуется аккуратное управление, особенно в контексте транзакций. При работе с транзакциями необходимо корректно закрывать подключение независимо от того, фиксируется транзакция или откатывается. Некорректные закрытия чреваты блокировками баз данных или исчерпанием пула подключений.

Вот пример управления транзакцией базы данных с помощью try-with-resources:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TransactionExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "username";
String password = "password";

try (Connection conn = DriverManager.getConnection(url, user, password)) {
conn.setAutoCommit(false); // Начинаем транзакцию

try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
stmt.executeUpdate("UPDATE accounts SET balance = balance + 100 WHERE id = 2");

conn.commit(); // Фиксируем транзакцию, если оба обновления успешны
System.out.println("Transaction committed successfully.");
} catch (SQLException e) {
conn.rollback(); // Откатываем транзакцию в случае сбоя
System.err.println("Transaction rolled back due to error: " + e.getMessage());
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

В этом примере с помощью try-with-resources управляется транзакция базы данных. Ресурсы Connection и Statement обрабатываются отдельно, чем обеспечиваются закрытие Statement после использования и закрытие Connection независимо от того, фиксируется транзакция или откатывается. Такой подход  —  надежный и лаконичный способ управления транзакциями баз данных, снижения риска утечки ресурсов и обеспечения согласованного поведения приложения.

Сетевые ресурсы: обработка нескольких сокетов

Отлично управляется try-with-resources и с сетевыми ресурсами вроде сокетов. Для корректного их закрытия сетевым подключениям часто требуется точное управление, при котором предотвращаются потенциальные проблемы вроде исчерпания портов или задержек подключения.

Вот пример обработки подключений нескольких сокетов с try-with-resources:

import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;

public class MultiSocketServerExample {
public static void main(String[] args) {
int port = 8080;

try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server is listening on port " + port);

while (true) {
try (Socket clientSocket = serverSocket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {

System.out.println("New client connected: " + clientSocket.getInetAddress());

String message;
while ((message = reader.readLine()) != null) {
System.out.println("Received: " + message);
}

System.out.println("Client disconnected: " + clientSocket.getInetAddress());
} catch (IOException e) {
System.err.println("Client connection error: " + e.getMessage());
}
}
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
}
}
}

Здесь с помощью ServerSocket на указанном порту прослушиваются входящие подключения. Для каждого клиентского подключения принимается новый Socket, а данные клиента считываются благодаря BufferedReader. Оператором try-with-resources корректно закрываются Socket и BufferedReader после отключения каждого клиента, чем предотвращается утечка ресурсов и сохраняется респонсивность сервера к новым подключениям.

Усовершенствованное управление ресурсами: сочетание файлового ввода-вывода и сетевых операций

Иногда приложениям требуется сочетание файлового ввода-вывода и сетевых операций: получение данных по сети и запись их в файл. Управлением таких сценариев с помощью try-with-resources корректно обрабатываются как файловые, так и сетевые ресурсы.

Вот пример, где полученные от клиента данные сохраняются в файле:

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;

public class NetworkToFileExample {
public static void main(String[] args) {
int port = 9090;

try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server listening on port " + port);

while (true) {
try (Socket clientSocket = serverSocket.accept();
InputStream inputStream = clientSocket.getInputStream();
FileOutputStream fos = new FileOutputStream("received_data.bin")) {

byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}

System.out.println("Data received and saved to file.");
} catch (IOException e) {
System.err.println("Error during data transfer: " + e.getMessage());
}
}
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
}
}
}

В этом примере по сетевому подключению сервером от клиента получаются двоичные данные и записываются прямо в файл. При управлении Socket, InputStream и FileOutputStream все ресурсы закрываются оператором try-with-resources корректно, даже если при передаче данных появляется ошибка. Таким подходом предотвращаются потенциальные проблемы: повреждение файлов, потеря данных или утечки подключения.

Заключение

Оператор try-with-resources в Java  —  мощный функционал, которым упрощается управление ресурсами, они закрываются автоматически, а возможность ошибок уменьшается. С освоением этого подхода пишется более чистый, сопровождаемый код для различных сценариев  —  от файлового ввода-вывода до транзакций баз данных и сетевых взаимодействий. С try-with-resources код не только упрощается, повышаются его надежность и эффективность. Так что это важный инструмент современной разработки на Java.

  1. Обзор функционала Java 7.
  2. Документация по интерфейсу AutoCloseable.
  3. Руководство по вводу-выводу Java.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Alexander Obregon: Java’s try-with-resources Statement Explained

Предыдущая статьяЭффективная стратегия тестирования Android-проектов. Часть 1
Следующая статьяКак сделать интернет-магазин из Spring Boot, Angular, MySQL и Jasper Reports