try-with-resources — tự đóng resource không rò rỉ
Cú pháp try-with-resources từ Java 7, interface AutoCloseable, thứ tự close nhiều resource, suppressed exception, và vì sao pattern close-trong-finally cổ điển dễ sai.
TL;DR: Mọi tài nguyên hệ thống (tệp tin, socket mạng, kết nối cơ sở dữ liệu) đều bắt buộc phải được giải phóng sau khi sử dụng để tránh cạn kiệt tài nguyên hệ điều hành. Cú pháp try-with-resources (từ Java 7) cho phép tự động giải phóng mọi đối tượng cài đặt interface AutoCloseable, giải quyết triệt để rủi ro rò rỉ bộ nhớ, loại bỏ bẫy ghi đè ngoại lệ (Exception Shadowing) và giảm thiểu mã lệnh rườm rà của khối finally truyền thống.
Ở bài trước, bạn đã thấy các thao tác I/O và cơ sở dữ liệu thường ném ra các Checked Exception như IOException hay SQLException. Việc xử lý các luồng thao tác này luôn đi kèm trách nhiệm bắt buộc: phải đóng tài nguyên dù thao tác thành công hay thất bại.
Trước Java 7, đóng tài nguyên trong khối finally là chuẩn mực duy nhất, nhưng nó lại là một trong những khối mã dễ sinh lỗi tiềm ẩn nhất trong Java. Hãy cùng đối chiếu cách tiếp cận cũ và giải pháp hiện đại của try-with-resources.
1. Tại sao mẫu đóng tài nguyên trong khối finally cũ lại dễ sinh lỗi?
Hãy quan sát đoạn mã đọc tệp danh mục ấn phẩm theo phong cách trước Java 7:
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("catalog.json"));
return br.readLine();
} catch (IOException e) {
logger.error("Doc tap tin that bai", e);
return null;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
logger.error("Loi khi dong tap tin", e);
}
}
}
Bốn nhược điểm chí mạng của mô hình này:
- Quá rườm rà (Boilerplate): Đoạn mã đóng tài nguyên chiếm nhiều dòng lệnh hơn cả nghiệp vụ đọc dữ liệu thực tế.
- Khởi tạo dở dang: Nếu lệnh
new FileReaderbị lỗi, biếnbrvẫn mang giá trịnull, bắt buộc phải có câu lệnhif (br != null)để tránhNullPointerExceptiontrong khốifinally. - Bẫy Exception Shadowing: Bản thân phương thức
close()cũng có thể némIOException. Nếu phương thứcbr.readLine()đã ném lỗi chính, việcclose()ném tiếp ngoại lệ sẽ xóa sạch hoàn toàn lỗi gốc. - Kim tự tháp lồng nhau (Pyramid of Doom): Khi làm việc với 2 hoặc 3 tài nguyên cùng lúc (ví dụ: đọc từ tệp đầu vào và ghi ra tệp đầu ra), số lượng khối
try-catchlồng nhau trongfinallytrở thành một cơn ác mộng bảo trì.

2. Cú pháp try-with-resources: Ngắn gọn và an toàn tuyệt đối
Java 7 giới thiệu cú pháp khai báo tài nguyên ngay trong cặp ngoặc đơn () sau từ khoá try:
public String readCatalogJson(String path) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
logger.error("Doc tap tin that bai: " + path, e);
return null;
}
}
Mã nguồn được rút ngắn từ 18 dòng xuống còn 6 dòng. Trình biên dịch javac sẽ tự động dịch chuyển đoạn mã trên thành cấu trúc nhị phân tương đương với logic hoàn chỉnh sau:
BufferedReader br = new BufferedReader(new FileReader(path));
Throwable primaryException = null;
try {
return br.readLine();
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (br != null) {
if (primaryException != null) {
try {
br.close();
} catch (Throwable suppressed) {
primaryException.addSuppressed(suppressed); // Khong de mat ngoai le chinh!
}
} else {
br.close();
}
}
}
Toàn bộ logic phức tạp về kiểm tra null và bảo toàn lỗi qua addSuppressed được trình biên dịch tự động sinh ra một cách chuẩn xác.
3. Interface AutoCloseable: Điều kiện để một đối tượng dùng được với try-with-resources
Bất kỳ lớp nào muốn xuất hiện trong mệnh đề try (...) bắt buộc phải cài đặt interface java.lang.AutoCloseable:
public interface AutoCloseable {
void close() throws Exception;
}
Hoặc interface con chuyên biệt cho I/O là java.io.Closeable:
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
Hầu hết các tài nguyên chuẩn trong JDK đều đã hỗ trợ sẵn:
- I/O Streams:
InputStream,OutputStream,Reader,Writer,Scanner. - Database (JDBC):
Connection,Statement,PreparedStatement,ResultSet. - Mạng:
Socket,ServerSocket,HttpClient(từ Java 21). - Đa luồng:
ExecutorService(từ Java 19 hỗ trợ tự động chờ hoàn thành tác vụ khi đóng).
Tự xây dựng tài nguyên quản lý vòng đời (Custom AutoCloseable)
Bạn có thể tự tạo các đối tượng quản lý tài nguyên tạm thời trong hệ thống thư viện, ví dụ: tạo khoá bảo trì danh mục (Catalog Maintenance Lock):
public class CatalogMaintenanceLock implements AutoCloseable {
private final String lockName;
public CatalogMaintenanceLock(String lockName) {
this.lockName = lockName;
logger.info("Khoa danh muc de bao tri: " + lockName);
}
@Override
public void close() {
logger.info("Giai phong khoa danh muc: " + lockName);
}
}
// Su dung an toan:
try (CatalogMaintenanceLock lock = new CatalogMaintenanceLock("MAIN_CATALOG")) {
rebuildIndex();
} // Tu dong giai phong khoa ngay khi ra khoi khoi try!
4. Quản lý nhiều tài nguyên và quy tắc thứ tự đóng
Khi làm việc với nhiều tài nguyên cùng lúc, bạn phân cách chúng bằng dấu chấm phẩy (;):
try (
FileInputStream in = new FileInputStream("source_catalog.dat");
FileOutputStream out = new FileOutputStream("backup_catalog.dat")
) {
in.transferTo(out);
}

Quy tắc quan trọng: Thứ tự đóng ngược lại với thứ tự mở
Trình biên dịch luôn đóng tài nguyên theo nguyên tắc LIFO (vào sau ra trước):
- Mở
intrước. - Mở
outsau. - Khi kết thúc: Đóng
outtrước, sau đó mới đóngin.
Quy tắc này bảo đảm tính toàn vẹn: tài nguyên phụ thuộc được đóng trước khi tài nguyên gốc bị hủy (ví dụ: đóng ResultSet rồi đến Statement, cuối cùng mới đóng Connection).
5. Nâng cấp trong Java 9: Biến Effectively Final
Trước Java 9, bạn bắt buộc phải khởi tạo biến mới ngay bên trong ngoặc tròn của try:
// Java 7 / 8: Bat buoc phai gan bien moi
BufferedReader br = getReader();
try (BufferedReader r = br) {
r.readLine();
}
Từ Java 9, nếu một biến tài nguyên đã được khởi tạo trước đó và không bị gán lại giá trị (effectively final), bạn có thể truyền trực tiếp tên biến vào:
// Java 9+: Ngan gon va truc quan
BufferedReader reader = getReader();
try (reader) {
reader.readLine();
}
6. Deep Dive Oracle & Tài liệu tham khảo
- Effective Java (Joshua Bloch) Item 9: "Prefer try-with-resources to try-finally" (Luôn ưu tiên try-with-resources hơn try-finally).
- JLS §14.20.3 (try-with-resources): Đặc tả chính thức về cơ chế khử đường (desugaring) của trình biên dịch cho cú pháp try-with-resources.
- AutoCloseable Javadoc: Khuyến nghị thiết kế phương thức
close()phải idempotent (gọi nhiều lần không gây lỗi phụ).
7. Tự kiểm tra kiến thức
- Q1Tại sao phương thức close() trong try-with-resources được gọi ngay cả khi thân khối try ném ra ngoại lệ?
- Q2Thứ tự đóng các tài nguyên khi khai báo nhiều đối tượng trong cùng một mệnh đề try-with-resources tuân theo quy tắc nào?
- Q3Cơ chế Suppressed Exception trong try-with-resources xử lý như thế nào khi cả thân hàm lẫn lệnh close() đều cùng ném ngoại lệ?
- Q4Điều kiện tiên quyết để một lớp đối tượng có thể được sử dụng trong khối try-with-resources là gì?
- Q5Tính năng nào được nâng cấp cho cú pháp try-with-resources kể từ phiên bản Java 9?
Bài tiếp theo: Custom exception — khi nào nên tạo và phân cấp exception domain
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Checked vs Unchecked — cuộc tranh luận lớn trong thiết kế Java
Bài tiếpCustom exception — thiết kế phân cấp exception nghiệp vụ
Lộ trình Junior Java Backend Developer
Chặng 4/21 · Exception handling — bài 5/7 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này