Mọi resource cần được đóng: file, socket, connection DB, stream. Quên đóng → rò rỉ handle, file lock, connection pool cạn, đôi khi crash app sau vài giờ. Trước Java 7, pattern đóng resource trong finally là chuẩn nhưng dài và dễ sai. Java 7 giới thiệu try-with-resources — cú pháp gọn mà compiler tự sinh finally đúng.
Bài này giải thích cú pháp, interface AutoCloseable, thứ tự close, suppressed exception, và so sánh với pattern cũ để thấy vì sao try-with-resources là default mới.
1. Pattern cũ — close trong finally
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("a.txt"));
return br.readLine();
} catch (IOException e) {
log.error("Read failed", e);
return null;
} finally {
if (br != null) {
try { br.close(); }
catch (IOException e) {
log.error("Close failed", e); // thuong bo qua hoac log
}
}
}
Vấn đề:
- Boilerplate: 5–7 dòng chỉ để đảm bảo close.
- Check null: biến khai báo ngoài try, nếu
new BufferedReader(...)ném exception thìbr= null →NullPointerExceptiontrong finally. - Try lồng trong finally:
close()cũng ném exception, phải bọc lại. Nếu không, exception close che exception chính trong try → mất thông tin quan trọng. - Nhiều resource → chồng tầng
finally— code pyramid khó đọc.
2. try-with-resources — cú pháp mới
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
return br.readLine();
} catch (IOException e) {
log.error("Read failed", e);
return null;
}
Khai resource trong () sau try. Compiler tự sinh finally đóng resource ngay khi block thoát — dù bình thường hay do exception.
Luồng thực tế compiler sinh (giản lược):
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
Throwable primary = null;
try {
return br.readLine();
} catch (Throwable t) {
primary = t;
throw t;
} finally {
if (br != null) {
if (primary != null) {
try { br.close(); }
catch (Throwable suppressed) { primary.addSuppressed(suppressed); }
} else {
br.close();
}
}
}
Bạn viết 3 dòng, compiler sinh 12 dòng đúng. Lợi ích:
- Không boilerplate — close tự động.
- Safe với null — resource chưa khởi tạo thành công thì skip close.
- Suppressed exception — exception close không ghi đè exception chính (xem phần 5).
3. Interface AutoCloseable
Resource dùng được với try-with-resources phải implement AutoCloseable:
public interface AutoCloseable {
void close() throws Exception;
}
Hoặc subtype Closeable (chỉ ném IOException, cho IO resource):
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
Hầu hết resource trong JDK đã implement:
- IO:
FileReader,BufferedReader,InputStream,OutputStream,Scanner,FileWriter. - Network:
Socket,ServerSocket,HttpClient(Java 11+). - DB:
Connection,Statement,ResultSet(JDBC). - Concurrency:
ExecutorService(Java 19+ với virtual thread).
3.1 Tự implement AutoCloseable
public class TempFile implements AutoCloseable {
private final Path path;
public TempFile(String prefix) throws IOException {
this.path = Files.createTempFile(prefix, ".tmp");
}
public Path getPath() { return path; }
@Override
public void close() throws IOException {
Files.deleteIfExists(path);
}
}
// Su dung:
try (TempFile t = new TempFile("upload")) {
Files.writeString(t.getPath(), "data");
// file tu xoa khi block ket thuc
}
4. Nhiều resource — dấu ; phân cách
try (
FileInputStream in = new FileInputStream("src.txt");
FileOutputStream out = new FileOutputStream("dst.txt")
) {
in.transferTo(out);
} catch (IOException e) {
log.error("Copy failed", e);
}
4.1 Thứ tự close — ngược thứ tự khai báo
Compiler đóng resource theo thứ tự ngược thứ tự khai:
Open: in -> out
Close: out -> in
Giống LIFO — resource mở sau đóng trước. Đúng với thực tế: nếu out phụ thuộc in (vd out là decorator của in), đóng out trước để flush rồi mới đóng in.
4.2 Biến effectively final từ Java 9
Java 9 cho phép dùng biến khai báo ngoài trong try-with-resources:
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
try (br) { // Java 9+ — br effectively final
return br.readLine();
}
Trước Java 9, phải khai báo mới trong ().
5. Suppressed exception — không mất thông tin
Bẫy pattern cũ: close() ném exception trong finally che exception chính của try.
// Pattern cu — MAT exception chinh
FileReader r = null;
try {
r = new FileReader("a.txt");
throw new RuntimeException("main");
} finally {
r.close(); // nem IOException -> che RuntimeException
}
// Caller chi thay IOException, khong biet RuntimeException "main" goc
Try-with-resources giải quyết bằng suppressed exception:
try (FileReader r = new FileReader("a.txt")) {
throw new RuntimeException("main");
}
// Caller bat duoc RuntimeException "main" (PRIMARY)
// close() nem IOException -> dinh kem vao primary qua addSuppressed()
// Stack trace: primary exception + "Suppressed:" block ben duoi
Print stack trace:
java.lang.RuntimeException: main
at ...
Suppressed: java.io.IOException: close error
at ...
Không mất thông tin — cả primary và suppressed đều đi chung stack trace. Access suppressed exception runtime:
Throwable[] suppressed = primary.getSuppressed();
6. Exception trong khi mở resource
try (FileReader r = new FileReader("nonexistent.txt")) {
// body khong chay
} catch (IOException e) {
log.error("Open failed", e);
}
Nếu new FileReader(...) ném exception, block body không chạy, catch handler match exception từ lúc mở. Try-with-resources xử lý đúng — không có resource nào cần close.
7. Stream API close — cần try-with-resources
try (Stream<String> lines = Files.lines(Path.of("a.txt"))) {
lines.filter(s -> s.startsWith("ERROR"))
.forEach(System.out::println);
}
// stream tu dong close -> file handle release
Files.lines trả Stream giữ file handle mở — phải close. Quên try-with-resources → rò rỉ handle. Với Files.readAllLines() không rò vì nó load sẵn list.
8. Khi nào KHÔNG cần try-with-resources?
- Không phải resource —
List,Map,Stringkhông có handle bên dưới, không cần close. - Resource nằm trong class dài tuổi (vd
ExecutorServicedùng cả app life) — close trong shutdown hook. - Resource không implement AutoCloseable — phải close thủ công (hiếm trong JDK modern).
9. Pitfall tổng hợp
❌ Nhầm 1: Close thủ công trong finally cho resource đã dùng try-with-resources.
try (BufferedReader br = ...) {
...
} finally {
br.close(); // COMPILE ERROR — br da out of scope, hoac nem IllegalStateException
}
✅ Để try-with-resources lo — không đụng close nữa.
❌ Nhầm 2: Quên try-with-resources cho Stream.lines.
Files.lines(path).forEach(...); // handle khong duoc dong
✅ try (Stream<String> s = Files.lines(path)) { s.forEach(...); }.
❌ Nhầm 3: Thứ tự close khi có dependency.
try (
OutputStream os = socket.getOutputStream();
Socket socket = new Socket(...) // cant — socket chua khai
)
✅ Khai resource độc lập trước, phụ thuộc sau — compiler tự đóng ngược thứ tự.
❌ Nhầm 4: Resource implement close() không idempotent.
public void close() {
if (closed) throw new IllegalStateException(); // close 2 lan -> nem
...
}
✅ close() nên idempotent: gọi 2 lần không lỗi. Đây là convention mạnh của AutoCloseable.
❌ Nhầm 5: Dùng try-with-resources cho variable không phải resource.
try (String s = "hello") { ... } // COMPILE ERROR — String khong AutoCloseable
✅ Chỉ AutoCloseable/Closeable.
10. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- JLS §14.20.3 — try-with-resources — cú pháp đầy đủ và desugaring.
- AutoCloseable — interface gốc.
- Closeable — subtype cho IO.
- JEP 213 — Milling Project Coin — mở rộng try-with-resources Java 9 (effectively final).
- Throwable.addSuppressed / getSuppressed — API suppressed exception.
- Effective Java Item 9: "Prefer try-with-resources to try-finally".
Ghi chú: Item 9 của Effective Java nói thẳng: try-with-resources "không chỉ ngắn hơn mà còn đúng hơn" pattern try-finally — vì desugaring xử lý suppressed exception, null-check, thứ tự close. Tác giả Bloch trực tiếp thiết kế feature này trong JDK 7. Rule rõ ràng: mọi resource dùng try-with-resources, không ngoại lệ.
11. Tóm tắt
- try-with-resources (Java 7+) thay pattern
try { ... } finally { close() }cho resource. - Cú pháp:
try (Type var = ...) { ... }. Compiler tự sinh finally đóng resource đúng. - Resource phải implement
AutoCloseable(close() throws Exception) hoặcCloseable(IO-specific). - Nhiều resource: phân cách bằng
;. Close theo thứ tự ngược khai báo. - Java 9+: dùng biến effectively final khai ngoài trong
try (var) { ... }. - Suppressed exception: nếu close ném exception khi đã có exception chính, close bị "suppress" và gắn vào primary — không mất thông tin.
- Luôn dùng try-with-resources cho
Files.lines,Streamcó resource,Connection,Socket... close()nên idempotent — gọi 2 lần không lỗi.- Rule: "mọi resource dùng try-with-resources".
12. Tự kiểm tra
Q1Đoạn sau có đủ đảm bảo resource được đóng không?BufferedReader br = new BufferedReader(new FileReader("a.txt"));
String line = br.readLine();
br.close();
▸
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
String line = br.readLine();
br.close();Không đủ. Nếu br.readLine() ném exception (IOException), execution dừng ngay — br.close() không bao giờ chạy → file handle rò rỉ.
Fix:
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
String line = br.readLine();
}Try-with-resources đảm bảo close chạy dù block body normal hoặc ném exception. Trước Java 7, pattern đúng là try/finally với close trong finally — dài gấp 3 lần và dễ sai.
Q2Thứ tự close trong đoạn sau là gì?try (
A a = new A();
B b = new B(a);
C c = new C(b)
) { ... }
▸
try (
A a = new A();
B b = new B(a);
C c = new C(b)
) { ... }Close theo thứ tự ngược khai báo: c.close() → b.close() → a.close().
Đây là LIFO — quan trọng khi có dependency: b wrap a, c wrap b. Close c trước để flush/release dependency của nó, rồi b có thể clean up đúng cách, cuối cùng a không còn ai giữ → close an toàn.
Đảo thứ tự (close a trước) có thể làm b cố flush vào a đã đóng → IllegalStateException.
Q3Đoạn sau in gì lên stack trace?try (AutoCloseable r = () -> { throw new IOException("close"); }) {
throw new RuntimeException("main");
}
▸
try (AutoCloseable r = () -> { throw new IOException("close"); }) {
throw new RuntimeException("main");
}Stack trace (giản lược):
java.lang.RuntimeException: main
at ...
Suppressed: java.io.IOException: close
at ...Flow:
- Body ném
RuntimeException("main"). - Compiler-generated finally gọi
r.close(). close()némIOException("close").- Vì đã có primary (
RuntimeException),IOExceptionđược suppress:primary.addSuppressed(ioException). - Caller bắt được
RuntimeException; suppressed hiển thị trong stack trace và truy cập quagetSuppressed().
Pattern cũ (close trong finally thủ công) sẽ mất RuntimeException vì IOException ném từ close sẽ propagate lên, phủ primary. Try-with-resources fix bug này.
Q4Viết class TempDirectory implement AutoCloseable tự xoá thư mục khi close.▸
TempDirectory implement AutoCloseable tự xoá thư mục khi close.import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
public class TempDirectory implements AutoCloseable {
private final Path path;
public TempDirectory(String prefix) throws IOException {
this.path = Files.createTempDirectory(prefix);
}
public Path getPath() { return path; }
@Override
public void close() throws IOException {
if (!Files.exists(path)) return; // idempotent
try (var stream = Files.walk(path)) {
stream.sorted(Comparator.reverseOrder()) // file truoc, dir sau
.forEach(p -> {
try { Files.deleteIfExists(p); }
catch (IOException e) { /* log or throw */ }
});
}
}
}
// Su dung:
try (TempDirectory td = new TempDirectory("upload")) {
Files.writeString(td.getPath().resolve("a.txt"), "data");
// thu muc va moi file trong tu xoa khi block ket thuc
}Ghi chú:
closeidempotent — checkFiles.existstrước khi xoá, gọi 2 lần không lỗi.Files.walktrảStreamgiữ file handle → wrap trong try-with-resources bên trongclose.reverseOrderđể xoá file trước, directory sau — thư mục rỗng mới xoá được.
Q5Đoạn sau dùng Files.lines — có bug gì?long count = Files.lines(Path.of("big.log"))
.filter(l -> l.contains("ERROR"))
.count();
▸
Files.lines — có bug gì?long count = Files.lines(Path.of("big.log"))
.filter(l -> l.contains("ERROR"))
.count();Bug: file handle không được đóng. Files.lines trả một Stream lazy giữ file handle mở — compiler không tự sinh close cho stream này khi dùng ngoài try-with-resources.
Với loop xử lý vài nghìn lần → handle pool cạn, OS report "too many open files" → app crash.
Fix:
long count;
try (var lines = Files.lines(Path.of("big.log"))) {
count = lines.filter(l -> l.contains("ERROR")).count();
}Alternative: Files.readAllLines(path) — đọc hết vào List<String> rồi return, file tự đóng. Nhưng với file lớn, tốn memory. Files.lines + try-with-resources là cách đúng cho file lớn.
Rule: bất kỳ API trả Stream có doc nói "must be closed" → dùng try-with-resources.
Bài tiếp theo: Custom exception — thiết kế hierarchy exception nghiệp vụ