Java OO & Functional
14/38
Bài 14 / 38~12 phútException HandlingLộ trình · chặng 04/21Miễ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.

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:

Java
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:

  1. 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ế.
  2. Khởi tạo dở dang: Nếu lệnh new FileReader bị lỗi, biến br vẫn mang giá trị null, bắt buộc phải có câu lệnh if (br != null) để tránh NullPointerException trong khối finally.
  3. Bẫy Exception Shadowing: Bản thân phương thức close() cũng có thể ném IOException. Nếu phương thức br.readLine() đã ném lỗi chính, việc close() ném tiếp ngoại lệ sẽ xóa sạch hoàn toàn lỗi gốc.
  4. 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-catch lồng nhau trong finally trở thành một cơn ác mộng bảo trì.

Hai cột so sánh: try-finally để lỗi close đè mất ngoại lệ nghiệp vụ ban đầu, try-with-resources lưu ngoại lệ thứ hai vào danh sách Suppressed

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:

Java
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:

Java
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:

Java
public interface AutoCloseable {
    void close() throws Exception;
}

Hoặc interface con chuyên biệt cho I/O là java.io.Closeable:

Java
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):

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

Java
try (
    FileInputStream in = new FileInputStream("source_catalog.dat");
    FileOutputStream out = new FileOutputStream("backup_catalog.dat")
) {
    in.transferTo(out);
}

Sơ đồ tuần tự minh họa thứ tự đóng tài nguyên LIFO: tài nguyên mở sau out được đóng trước, in đóng sau

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):

  1. Mở in trước.
  2. Mở out sau.
  3. Khi kết thúc: Đóng out trước, sau đó mới đóng in.

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
// 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
// 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

Tài liệu tham khảo đặc tả
  • 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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tạ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ệ?
  2. Q2
    Thứ 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?
  3. Q3
    Cơ 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ệ?
  4. 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ì?
  5. Q5
    Tí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.

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

Bài tiếp theo

Custom exception — thiết kế phân cấp exception nghiệp vụ