99 Ngày Java — Ngày 44: try-with-resources
Ngày 42 ta gặp cái bẫy thứ ba của finally: exception ném ra lúc đóng tài nguyên che mất lỗi gốc. Hôm nay là lời giải chính thức cho vấn đề đó — try-with-resources. Nó không chỉ ngắn hơn: nó sửa một lỗi mà bản viết tay gần như luôn mắc phải, bằng cơ chế suppressed exception — giữ nguyên lỗi gốc và đính lỗi lúc đóng vào bên cạnh.

Rò rỉ tài nguyên và cái giá của finally viết tay
Rò rỉ tài nguyên là loại bug đặc biệt khó chịu vì nó không nổ ngay. Ứng dụng chạy êm vài ngày, rồi đột ngột báo hết file handle hoặc cạn pool kết nối — thường vào đúng giờ đông người dùng nhất.
// Trước Java 7, đóng tài nguyên đúng cách trông như thế này:
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(duongDan));
return br.readLine();
} catch (IOException e) {
throw new AppException("Không đọc được " + duongDan, e);
} finally {
if (br != null) { // ① phải kiểm tra null
try { br.close(); } // ② close() CŨNG ném IOException
catch (IOException e) { /* nuốt luôn?? */ } // ③ và ta nuốt nó
}
}
// Sáu dòng chỉ để đóng một cái file. Và bản này VẪN CÒN LỖI:
// nếu readLine() ném exception rồi close() cũng ném, thì lỗi từ close()
// GHI ĐÈ lỗi gốc — đúng cái bẫy số 3 của finally ở Ngày 42.
// Java 7 gói tất cả vào một dòng:
try (var br = new BufferedReader(new FileReader(duongDan))) {
return br.readLine();
} // ❗ close() TỰ ĐỘNG được gọi, kể cả khi có exception
// Rò rỉ tài nguyên là loại bug đặc biệt khó chịu vì nó KHÔNG nổ ngay:
// ứng dụng chạy êm vài ngày, rồi đột nhiên "Too many open files"
// hoặc pool kết nối cạn sạch vào đúng giờ cao điểm.Bản
finallyviết tay tốn sáu dòng, và vẫn còn lỗi: lỗi lúc đóng ghi đè mất lỗi gốc.Đó chính là cái bẫy thứ ba của
finallyở Ngày 42 — và cũng là lý do trực tiếp khiếntry-with-resourcesra đời.Một dòng thay cho sáu dòng, mà lại đúng hơn — hiếm khi có sự đánh đổi nào dễ chọn đến vậy.
Cú pháp, thứ tự đóng và suppressed exception
Khai nhiều tài nguyên trong cùng một try thì chúng được đóng theo thứ tự ngược — đúng như bạn cần khi tài nguyên sau phụ thuộc vào tài nguyên trước. Và điểm hay nhất là cách nó xử lý lỗi chồng lỗi.
// Cú pháp đầy đủ — khai nhiều tài nguyên, cách nhau bằng dấu chấm phẩy:
try (var in = Files.newInputStream(nguon);
var out = Files.newOutputStream(dich)) {
in.transferTo(out);
}
// ❗ ĐÓNG THEO THỨ TỰ NGƯỢC: out đóng trước, rồi mới tới in.
// Đúng như bạn muốn — luồng ghi phải được đóng (và flush) trước luồng đọc.
// Điều kiện duy nhất: class phải implements AutoCloseable (hoặc Closeable).
class KetNoiCuaToi implements AutoCloseable {
@Override public void close() { log.info("đang dọn dẹp"); }
}
try (var kn = new KetNoiCuaToi()) { ... } // chạy được ngay
// Java 9 cho phép dùng biến đã khai sẵn, miễn là nó final hoặc effectively final:
var kn = moKetNoi();
try (kn) { ... } // hợp lệ từ Java 9
// SUPPRESSED EXCEPTION — điều mà finally viết tay không làm được:
try (var r = new TaiNguyenLoi()) {
throw new RuntimeException("lỗi GỐC"); // ← cái ta cần thấy
}
// Nếu close() cũng ném, JVM giữ NGUYÊN lỗi gốc và ĐÍNH lỗi kia vào:
// Exception in thread "main" java.lang.RuntimeException: lỗi GỐC
// Suppressed: java.io.IOException: lỗi khi đóng ← không mất!
// Truy cập bằng tay: e.getSuppressed() trả về mảng các lỗi bị nén.Đóng ngược thứ tự khai báo: luồng ghi đóng trước luồng đọc, kết quả truy vấn đóng trước câu lệnh và kết nối.
Chỉ cần
AutoCloseablelà class của bạn dùng được ngay — thêm một interface một method nữa đáng cài, sauIterablecủa Ngày 37.Suppressed exception là điểm không thể thay thế: lỗi gốc được giữ nguyên, lỗi lúc đóng đính vào bên cạnh, không mất thông tin nào.
Từ Java 9, biến đã khai sẵn (final hoặc effectively final) dùng thẳng trong
try (kn)được.
Dùng trong thực tế và bốn điều cần nhớ
Ngoài file, try-with-resources còn dùng cho kết nối database, socket, và một chỗ rất hay bị quên: stream của java.nio.file. Chúng trông giống stream bình thường, nhưng bên dưới đang giữ một handle thư mục.
// Ở đâu cũng dùng được, không riêng gì file:
try (var conn = dataSource.getConnection();
var ps = conn.prepareStatement(sql);
var rs = ps.executeQuery()) {
while (rs.next()) { ... }
} // ba tài nguyên, đóng ngược thứ tự, không rò rỉ
// Stream của java.nio.file cũng LÀ tài nguyên — đây là chỗ hay bị quên nhất:
try (var duongDans = Files.walk(thuMuc)) { // ❗ PHẢI đóng
duongDans.filter(Files::isRegularFile).forEach(...);
}
// Files.walk, Files.list, Files.lines đều mở handle thư mục.
// Quên đóng thì rò rỉ, và trên Windows còn KHÓA luôn thư mục đó.
// Ngược lại, stream từ collection (list.stream()) KHÔNG cần đóng.
// BỐN ĐIỀU CẦN NHỚ:
// 1. Đừng bọc try-with-resources quanh thứ KHÔNG phải của bạn:
try (var conn = ketNoiDuocTruyenVao) { ... } // ❌ bạn vừa đóng tài nguyên của người khác
// 2. close() nên IDEMPOTENT — gọi hai lần không được nổ.
// 3. close() ĐỪNG ném exception nếu tránh được; nếu buộc phải, hãy để nó
// ném đúng loại nghiệp vụ chứ không phải một lỗi mơ hồ.
// 4. Trong Spring, JdbcTemplate/JdbcClient (Ngày 35) và JPA đã tự quản lý
// kết nối — đừng tự mở connection rồi tự đóng, trừ khi có lý do rõ ràng.Files.walk,Files.list,Files.linesphải đóng; cònlist.stream()từ collection thì không cần.Đừng đóng tài nguyên không phải của mình: nhận một kết nối từ ngoài rồi bọc
try-with-resourceslà đóng đồ của người khác.close()nên idempotent — gọi hai lần không được nổ, vì trong nhiều luồng xử lý nó bị gọi nhiều hơn bạn tưởng.Trong Spring thì
JdbcClientvà JPA đã quản lý kết nối rồi — tự mở tự đóng chỉ khi có lý do rõ ràng.
Bài tập nhỏ
Viết lại một đoạn
finallyđóng file thànhtry-with-resourcesvà đếm số dòng tiết kiệm được.Tạo class
AutoCloseablein log trongclose(), khai hai cái trong mộttry— quan sát thứ tự đóng ngược lại.Ném exception trong khối
tryvà ném thêm một cái nữa trongclose(), rồi ingetSuppressed().Làm lại điều đó bằng
finallyviết tay để thấy lỗi gốc biến mất hoàn toàn.Gọi
Files.walktrong một vòng lặp mười nghìn lần mà không đóng — quan sát ứng dụng cạn file handle.
Kết lại
Bốn ý gói lại hôm nay: try-with-resources thay sáu dòng finally bằng một dòng và đúng hơn bản viết tay; nhiều tài nguyên thì đóng theo thứ tự ngược, và chỉ cần AutoCloseable là class của bạn tham gia được; suppressed exception giữ lại lỗi gốc thay vì để lỗi lúc đóng ghi đè — chính là chỗ finally viết tay không làm nổi; còn trong thực tế thì đừng quên Files.walk cũng là tài nguyên, và đừng đóng thứ không phải của mình. Ngày 45 ta bước vào phần I/O thật sự: đọc ghi file với java.io — stream so với reader, vì sao buffered lại nhanh hơn hàng chục lần, và cái bẫy charset. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


