Khối Lệnh try-with-resources (The try-with-resources Statement)
Câu lệnh try-with-resources trong Java là một tính năng giúp đơn giản hóa việc quản lý tài nguyên (như tệp, kết nối mạng, kết nối cơ sở dữ liệu, v.v.) bằng cách tự động đóng các tài nguyên đó khi kết thúc khối try, ngay cả khi có ngoại lệ xảy ra. Điều này giúp tránh rò rỉ tài nguyên và làm cho mã nguồn của bạn an toàn và dễ bảo trì hơn.
Rò rỉ tài nguyên là gì?
Rò rỉ tài nguyên (resource leak) là tình huống khi một tài nguyên (chẳng hạn như bộ nhớ, tệp tin, kết nối mạng, kết nối cơ sở dữ liệu, v.v.) không được giải phóng đúng cách sau khi sử dụng, dẫn đến việc tài nguyên này vẫn bị chiếm dụng mà không thể sử dụng lại hoặc tái chế cho các phần khác của chương trình. Điều này có thể gây ra các vấn đề như:
↳ Cạn kiệt tài nguyên: Nếu nhiều tài nguyên không được giải phóng, hệ thống có thể cạn kiệt tài nguyên, làm cho chương trình hoặc hệ điều hành không còn đủ tài nguyên để hoạt động bình thường.
↳ Giảm hiệu suất: Việc không giải phóng tài nguyên có thể làm giảm hiệu suất của chương trình vì các tài nguyên như bộ nhớ hoặc kết nối bị chiếm dụng mà không được tái sử dụng.
↳ Lỗi chương trình: Trong một số trường hợp, rò rỉ tài nguyên có thể dẫn đến lỗi chương trình hoặc gây ra các hành vi không mong muốn.
Vấn đề trước khi có try-with-resources
↳ Rò rỉ tài nguyên: Nếu ta không đóng các tài nguyên sau khi sử dụng, chúng vẫn chiếm dụng bộ nhớ và gây ra tình trạng rò rỉ tài nguyên.
↳ Quên đóng tài nguyên: Việc đóng các tài nguyên thường được thực hiện trong khối finally. Tuy nhiên, ta có thể quên đóng hoặc xử lý ngoại lệ không đúng cách, dẫn đến rò rỉ.
try-with-resources giải quyết vấn đề như thế nào?
↳ Tự động đóng tài nguyên: Khi khai báo tài nguyên ngay trong dấu ngoặc của câu lệnh try, Java sẽ tự động gọi phương thức close() cho tài nguyên đó khi kết thúc khối try, dù có ngoại lệ xảy ra hay không.
↳ Đơn giản hóa code: Không cần phải viết khối finally để đóng tài nguyên nữa.
↳ An toàn hơn: Giảm thiểu rủi ro quên đóng tài nguyên, dẫn đến rò rỉ.
Điều kiện để sử dụng try-with-resources:
↳ Tài nguyên được khai báo trong câu lệnh try phải là đối tượng của một lớp cài đặt giao diện AutoCloseable (hoặc Closeable). Điều này đảm bảo rằng tài nguyên sẽ tự động đóng khi khối try kết thúc.
Cú pháp của try-with-resources
Cú pháp
try (
// Khai báo các tài nguyên ở đây, ví dụ:
FileReader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr)
) {
// Sử dụng các tài nguyên trong khối try
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
// Xử lý ngoại lệ
e.printStackTrace();
}
Giải thích:
↳ try ( ... ): Bắt đầu một khối try đặc biệt.
↳ Khai báo tài nguyên: Bên trong dấu ngoặc, bạn khai báo các đối tượng cần đóng sau khi sử dụng. Các đối tượng này phải thực hiện giao diện AutoCloseable (ví dụ: FileReader, BufferedReader, Connection, ...).
↳ Khối try: Ở đây, bạn thực hiện các thao tác với các tài nguyên đã khai báo.
↳ Khối catch (tùy chọn): Xử lý các ngoại lệ có thể xảy ra trong quá trình sử dụng tài nguyên.
Dưới đây là chương trình Java hoàn chỉnh để thực hiện việc liệt kê tên các mục trong một tệp ZIP và ghi chúng vào một tệp văn bản, sử dụng câu lệnh try-with-resources:
Ví dụ: Example.java
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Example {
public static void writeToFileZipFileContents(String zipFileName, String outputFileName) throws IOException {
// Sử dụng mã hóa ký tự ASCII
Charset charset = Charset.forName("US-ASCII");
// Tạo đối tượng Path cho tệp đầu ra
Path outputFilePath = Paths.get(outputFileName);
// Mở tệp zip và tạo tệp đầu ra với câu lệnh try-with-resources
try (
// Mở tệp ZIP
ZipFile zf = new ZipFile(zipFileName);
// Tạo BufferedWriter để ghi vào tệp đầu ra với mã hóa ASCII
BufferedWriter writer = Files.newBufferedWriter(outputFilePath, charset)
) {
// Liệt kê từng mục (tệp hoặc thư mục) trong tệp ZIP
for (Enumeration<? extends ZipEntry> entries = zf.entries(); entries.hasMoreElements();) {
// Lấy tên mục và ghi vào tệp đầu ra
String newLine = System.lineSeparator();
String zipEntryName = entries.nextElement().getName() + newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
public static void main(String[] args) {
// Đường dẫn đến tệp ZIP và tệp văn bản đầu ra
String zipFileName = "example.zip";
String outputFileName = "output.txt";
try {
// Gọi phương thức để ghi tên các mục trong tệp ZIP vào tệp văn bản
writeToFileZipFileContents(zipFileName, outputFileName);
System.out.println("Đã ghi tên các mục trong tệp ZIP vào tệp " + outputFileName);
} catch (IOException e) {
System.err.println("Đã xảy ra lỗi: " + e.getMessage());
}
}
}
Để chạy chương trình này, bạn chỉ cần biên dịch và thực thi nó trong môi trường Java. Đảm bảo rằng tệp ZIP (example.zip) tồn tại trong thư mục hiện tại của bạn, hoặc chỉ định đường dẫn đầy đủ đến tệp ZIP. Tệp đầu ra (output.txt) sẽ được tạo trong cùng thư mục với chương trình hoặc theo đường dẫn mà bạn chỉ định.
Dưới đây là một chương trình Java hoàn chỉnh sử dụng JDBC để kết nối với cơ sở dữ liệu, thực hiện truy vấn và in kết quả từ một bảng trong cơ sở dữ liệu:
Yêu cầu: Trước khi chạy chương trình, đảm bảo rằng bạn đã cấu hình kết nối JDBC với cơ sở dữ liệu. Bạn sẽ cần JDBC driver tương ứng cho cơ sở dữ liệu của mình (ví dụ: MySQL, PostgreSQL, Oracle, v.v.).
Ví dụ: Example.java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class Example {
// Đổi thông tin kết nối theo cơ sở dữ liệu của bạn
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
try (Connection con = DriverManager.getConnection(URL, USER, PASSWORD)) {
viewTable(con);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void viewTable(Connection con) throws SQLException {
String query = "SELECT COF_NAME, SUP_ID, PRICE, SALES, TOTAL FROM COFFEES";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
int supplierID = rs.getInt("SUP_ID");
float price = rs.getFloat("PRICE");
int sales = rs.getInt("SALES");
int total = rs.getInt("TOTAL");
System.out.println(coffeeName + ", " + supplierID + ", " +
price + ", " + sales + ", " + total);
}
} catch (SQLException e) {
printSQLException(e);
}
}
private static void printSQLException(SQLException e) {
// In chi tiết thông báo lỗi
System.err.println("SQL State: " + e.getSQLState());
System.err.println("Error Code: " + e.getErrorCode());
System.err.println("Message: " + e.getMessage());
Throwable t = e.getCause();
while (t != null) {
System.out.println("Cause: " + t);
t = t.getCause();
}
}
}
Chạy chương trình:
↳ Cài đặt JDBC driver: Đảm bảo rằng bạn đã thêm JDBC driver vào classpath của bạn.
↳ Biên dịch và chạy: Biên dịch chương trình với javac và chạy với java. Đảm bảo rằng cơ sở dữ liệu của bạn đang chạy và có bảng COFFEES với các cột tương ứng.
Ngoại lệ bị ẩn trong try-with-resources
Ngoại lệ bị ẩn (suppressed exceptions) là những ngoại lệ xảy ra trong quá trình đóng các tài nguyên trong câu lệnh try-with-resources nhưng không được ném ra trực tiếp. Thay vào đó, ngoại lệ chính (xảy ra trong khối try) sẽ được ném ra, và các ngoại lệ khác sẽ bị ẩn đi.
Nguyên nhân:
↳ Khi một ngoại lệ xảy ra trong khối try, quá trình đóng các tài nguyên sẽ được thực hiện.
↳ Nếu có ngoại lệ xảy ra trong quá trình đóng tài nguyên, ngoại lệ này sẽ bị ẩn đi, và ngoại lệ từ khối try sẽ được ném ra.
Cách lấy lại ngoại lệ bị ẩn:
↳ Sử dụng phương thức Throwable.getSuppressed() trên ngoại lệ chính để lấy danh sách các ngoại lệ bị ẩn.
Ví dụ
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
// ...
} catch (IOException e) {
// Xử lý ngoại lệ chính
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("Ngoại lệ bị ẩn: " + suppressed);
}
}
Các lớp thực hiện giao diện AutoCloseable hoặc Closeable:
↳ Giao diện AutoCloseable là giao diện cơ bản yêu cầu các lớp thực hiện phương thức close() để đóng tài nguyên.
↳ Giao diện Closeable kế thừa từ AutoCloseable và thêm phương thức close() ném ngoại lệ IOException.
↳ Nhiều lớp trong Java thư viện chuẩn thực hiện giao diện Closeable, như FileReader, BufferedReader, InputStream, OutputStream, Connection, v.v.