Java OO & Functional/try-with-resources — tự đóng resource không rò rỉ
14/38
Bài 14 / 38~15 phútException HandlingMiễn phí lượt xem

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.

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 → NullPointerException trong 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 chỉ implement AutoCloseable từ Java 21 (bản thân class có từ 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 ().

⚠️ Bẫy logic: dùng tham chiếu sau khi khối try-with-resources kết thúc

Cú pháp truyền biến try (br) ngoài rất tiện, nhưng cần nhớ: ngay khi khối try-with-resources kết thúc, br đã bị đóng. Nếu tiếp tục gọi đọc/ghi trên br ở các dòng phía dưới:

String line = br.readLine(); // IOException: Stream closed

Tránh dùng lại biến tham chiếu resource sau khi khối try đã kết thúc.

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.

Hai luồng khi cả thân try lẫn close() đều ném lỗi:

Hai cột: try-finally có thân try ném E1 rồi close ném E2 làm finally kết thúc đột ngột nên E1 bị bỏ và caller chỉ nhận E2; try-with-resources cũng ném E1 rồi E2 nhưng gọi addSuppressed nên caller nhận E1 với E2 nằm trong khối Suppressed

Điểm cần nhớ: try-with-resources không làm lỗi nào biến mất — nó chỉ thôi vứt lỗi đi.

// 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 resourceList, Map, String không có handle bên dưới, không cần close.
  • Resource nằm trong class dài tuổi (vd ExecutorService dù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
}

✅ Để 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("Already closed"); // second close -> throws
    ...
}

close() nên idempotent: gọi nhiều lần không gây ra lỗi phụ nào.

Tại sao điều này quan trọng? Trong Java, các lớp Decorator (như BufferedReader bọc ngoài FileReader) được thiết kế theo nguyên lý LIFO. Khi khối try-with-resources kết thúc, compiler sẽ tự động gọi hàm close() của decorator ngoài (BufferedReader). Bản thân BufferedReader.close() sẽ gọi lan truyền xuống và đóng luôn resource bên trong (FileReader.close()).

Nếu cả hai resource đều được khai báo trong try-with-resources:

try (FileReader fr = new FileReader("a.txt");
     BufferedReader br = new BufferedReader(fr)) { ... }

Thì theo thứ tự ngược (LIFO), compiler sẽ gọi br.close() (đóng cả brfr), rồi sau đó tiếp tục gọi fr.close() lần thứ hai. Các Reader/Stream trong JDK đều implement close() idempotent — lần gọi thứ hai chỉ là no-op, nên đoạn trên chạy bình thường. Nhưng nếu bạn tự viết resource mà close() ném exception khi bị gọi lại (như ví dụ ❌ ở trên), lần đóng trùng sẽ ném IllegalStateException ngoài ý muốn. Vì vậy javadoc của AutoCloseable khuyến nghị implement close() idempotent.

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:

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ặc Closeable (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, Stream có 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

Tự kiểm tra
0/5 câu đã trả lời
  1. 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();
  2. Q2
    Thứ tự close trong đoạn sau là gì?
    try (
        A a = new A();
        B b = new B(a);
        C c = new C(b)
    ) { ... }
  3. Q3
    Đoạn sau in gì lên stack trace?
    try (AutoCloseable r = () -> { throw new IOException("close"); }) {
        throw new RuntimeException("main");
    }
  4. Q4
    Viết class TempDirectory implement AutoCloseable tự xoá thư mục khi close.
  5. 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();

Bài tiếp theo: Custom exception — thiết kế hierarchy exception nghiệp vụ

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.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Custom exception — thiết kế hierarchy exception nghiệp vụ