Java — Từ Zero đến Senior/Exception Handling/try-with-resources — tự đóng resource không rò rỉ
3/5
~15 phútException Handling

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 (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 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, 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:

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
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();

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.

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)
) { ... }

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");
}

Stack trace (giản lược):

java.lang.RuntimeException: main
    at ...
    Suppressed: java.io.IOException: close
        at ...

Flow:

  1. Body ném RuntimeException("main").
  2. Compiler-generated finally gọi r.close().
  3. close() ném IOException("close").
  4. Vì đã có primary (RuntimeException), IOException được suppress: primary.addSuppressed(ioException).
  5. Caller bắt được RuntimeException; suppressed hiển thị trong stack trace và truy cập qua getSuppressed().

Pattern cũ (close trong finally thủ công) sẽ mất RuntimeExceptionIOException ném từ close sẽ propagate lên, phủ primary. Try-with-resources fix bug này.

Q4
Viết class 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ú:

  • close idempotent — check Files.exists trước khi xoá, gọi 2 lần không lỗi.
  • Files.walk trả Stream giữ file handle → wrap trong try-with-resources bên trong close.
  • 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();

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ụ