CodeGym /Các khóa học /JAVA 25 SELF /Làm việc với try-with-resources: đóng tài nguyên tự động

Làm việc với try-with-resources: đóng tài nguyên tự động

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Làm việc với tệp trong Java (và không chỉ thế!) luôn là làm việc với các tài nguyên bên ngoài. Khi bạn mở một tệp, hệ điều hành cấp cho chương trình của bạn “descriptor” — một định danh đặc biệt cho phép đọc và ghi vào tệp. Số lượng descriptor như vậy là hữu hạn: nếu không đóng tệp, chương trình có thể nhanh chóng “ăn” hết tài nguyên sẵn có và bắt đầu ném ra những lỗi khó hiểu như "Too many open files".

Hơn nữa, nếu tệp không được đóng, nó có thể bị khóa đối với các chương trình khác. Ví dụ, bạn mở tệp để ghi, quên đóng nó, và giờ thì cả bạn lẫn người khác đều không thể sửa hay xóa nó. Kiểu “bắt con tin vĩnh viễn” trong thế giới hệ thống tệp.

Ví dụ thực tế

FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... làm vài việc, rồi quên gọi fis.close()

Nếu không gọi phương thức close(), tệp sẽ “treo” cho tới khi chương trình kết thúc. Trong các ứng dụng lớn, điều này có thể dẫn đến rò rỉ tài nguyên và thậm chí làm ứng dụng sập.

2. Cách cũ: finally + close()

Trước Java 7, cách kinh điển để đảm bảo đóng tệp trông như sau:

FileInputStream fis = null;
try {
    fis = new FileInputStream("data.txt");
    // đọc tệp
    int b = fis.read();
    // ...
} catch (IOException e) {
    System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            System.out.println("Lỗi khi đóng tệp: " + e.getMessage());
        }
    }
}

Nhược điểm của cách này

  • Dễ quên khối finally và gây rò rỉ tài nguyên.
  • Nhiều mã thừa, đặc biệt khi có nhiều luồng.
  • Nếu khi đóng phát sinh ngoại lệ, bạn cũng phải bắt riêng ngoại lệ đó.
  • Mã trở nên cồng kềnh và khó đọc hơn.

3. Cách hiện đại: try-with-resources

May mắn là từ Java 7 có cú pháp giải quyết những vấn đề này một cách tao nhã và tự động — try-with-resources.

Trông như thế nào

try (FileInputStream fis = new FileInputStream("data.txt")) {
    int b = fis.read();
    // làm việc với tệp
} catch (IOException e) {
    System.out.println("Lỗi khi thao tác với tệp: " + e.getMessage());
}
// Ở đây fis đã được đóng tự động!

Điểm mấu chốt: mọi tài nguyên được khai báo trong dấu ngoặc tròn sau try sẽ được tự động đóng sau khi khối kết thúc — ngay cả khi giữa chừng có ngoại lệ. Không cần viết finally, không cần bắt riêng lỗi khi đóng — Java sẽ làm hết cho bạn.

Những lớp nào có thể dùng trong try-with-resources?

Bất kỳ lớp nào triển khai giao diện AutoCloseable (hoặc Closeable cũ). Đó là hầu hết các luồng vào/ra chuẩn: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter, Scanner, PrintWriter và nhiều lớp khác.

4. Cú pháp try-with-resources: chi tiết và ví dụ

Một tài nguyên

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("Lỗi: " + e.getMessage());
}
// reader được đóng tự động!

Nhiều tài nguyên

Có thể khai báo nhiều tài nguyên cùng lúc, cách nhau bởi dấu chấm phẩy:

try (
    BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
    BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line);
        writer.newLine();
    }
} catch (IOException e) {
    System.out.println("Lỗi khi sao chép: " + e.getMessage());
}
// cả hai stream đã được đóng!

Thứ tự đóng: tài nguyên được đóng theo thứ tự ngược với thứ tự khai báo. Trước tiên sẽ gọi writer.close(), sau đó reader.close(). Điều này quan trọng nếu một luồng phụ thuộc vào luồng khác.

Sử dụng với lớp tùy chỉnh

Nếu bạn tự viết một lớp làm việc với tài nguyên, chỉ cần triển khai giao diện AutoCloseable:

class MyResource implements AutoCloseable {
    public void doSomething() {
        System.out.println("Đang làm việc với tài nguyên!");
    }

    @Override
    public void close() {
        System.out.println("Tài nguyên đã được đóng!");
    }
}

try (MyResource res = new MyResource()) {
    res.doSomething();
}
// Sau khi thoát khỏi khối sẽ in ra: "Tài nguyên đã được đóng!"

5. Cơ chế hoạt động: sơ đồ

flowchart TD
    A[Mở tài nguyên trong try-with-resources] --> B{Có phát sinh ngoại lệ trong khối try?}
    B -- Không --> C[Tài nguyên được đóng tự động]
    B -- Có --> D[Tài nguyên được đóng tự động]
    D --> E[Ngoại lệ được ném tiếp]
    C --> F[Chương trình tiếp tục thực thi]

Kết luận: bất kể có lỗi hay không, tài nguyên luôn được đóng!

6. Ví dụ: viết lại mã “theo cách mới”

Trước đây (phong cách cũ):

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("input.txt"));
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("Lỗi: " + e.getMessage());
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            System.out.println("Lỗi khi đóng: " + e.getMessage());
        }
    }
}

Bây giờ (try-with-resources):

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("Lỗi: " + e.getMessage());
}
// Thế là xong, không cần finally nữa!

7. Điều gì xảy ra khi có lỗi lúc đóng?

Đôi khi chính thao tác đóng tài nguyên có thể ném ngoại lệ (ví dụ, nếu tệp bất ngờ biến mất). Trong try-with-resources, các ngoại lệ như vậy không bị mất: nếu trong khối try đã có một ngoại lệ, và khi đóng tài nguyên phát sinh một ngoại lệ thứ hai, ngoại lệ thứ hai sẽ được thêm vào như “bị kìm nén” (suppressed exception) cho ngoại lệ chính. Có thể xem bằng phương thức Throwable.getSuppressed().

Ví dụ

try (MyResource res = new MyResource()) {
    throw new IOException("Lỗi trong khối try");
} catch (IOException e) {
    System.out.println("Lỗi chính: " + e.getMessage());
    for (Throwable suppressed : e.getSuppressed()) {
        System.out.println("Ngoại lệ bị kìm nén: " + suppressed.getMessage());
    }
}

8. Những lớp nào hỗ trợ try-with-resources?

Rất đơn giản: bất kỳ lớp nào triển khai giao diện AutoCloseable. Dưới đây chỉ là một vài lớp chuẩn:

Lớp Mục đích
FileInputStream
Đọc byte từ tệp
FileOutputStream
Ghi byte vào tệp
FileReader/FileWriter
Đọc/ghi văn bản
BufferedReader/Writer
Đệm (buffer) cho stream
PrintWriter
Ghi văn bản có định dạng
Scanner
Đọc dữ liệu từ tệp/console
ObjectInputStream/Output
Tuần tự hóa/giải tuần tự
ZipInputStream/Output
Làm việc với tệp nén ZIP
Socket, ServerSocket
Kết nối mạng

Nếu bạn dùng thư viện bên thứ ba — hãy xem tài liệu: nếu có phương thức close(), rất có thể lớp đó hỗ trợ try-with-resources.

9. Mẹo và lưu ý hữu ích

Có thể khai báo biến ngoài try (từ Java 9): có thể dùng tài nguyên đã được khai báo sẵn nếu chúng là final hoặc “effectively final”:

BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
    // ...
}

Không chỉ với tệp: try-with-resources hữu ích cho mọi tài nguyên: kết nối mạng, cơ sở dữ liệu, bất kỳ đối tượng nào có phương thức close().

Đừng bỏ qua ngoại lệ: ngay cả với try-with-resources cũng đừng quên bắt và xử lý ngoại lệ — đây không phải là “thuốc tiên”, chỉ là cách thuận tiện để tránh rò rỉ.

Đừng đóng tài nguyên thủ công bên trong try: không cần thiết — Java sẽ làm thay bạn! Nếu gọi close() thủ công, rồi khối try kết thúc, sẽ có thêm một lần cố gắng đóng tài nguyên đã đóng. Thường là an toàn, nhưng dễ gây bối rối.

10. Lỗi thường gặp khi dùng try-with-resources

Lỗi số 1: quên dùng try-with-resources. Nếu bạn vẫn viết finally { resource.close(); } — hoặc bạn đang ở năm 2011, hoặc bạn chưa đọc bài giảng này! Hãy dùng cú pháp hiện đại.

Lỗi số 2: khai báo tài nguyên ở ngoài try và chỉ dùng bên trong. Mã như vậy sẽ không tự động đóng tài nguyên:

BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
    // ... sử dụng reader
} finally {
    // Ở đây lại quên đóng!
}

Lỗi số 3: gọi close() thủ công bên trong khối try. Không nghiêm trọng, nhưng dư thừa và có thể dẫn đến đóng hai lần. Hãy tin tưởng Java.

Lỗi số 4: chỉ bắt Exception và bỏ qua đặc thù IO. Tốt hơn là bắt các ngoại lệ cụ thể (FileNotFoundException, IOException) để đưa thông điệp dễ hiểu cho người dùng.

Lỗi số 5: không xử lý ngoại lệ bị kìm nén. Nếu khi đóng tài nguyên phát sinh lỗi, nó có thể bị “kìm nén”. Nếu bạn phân tích lỗi, đừng quên getSuppressed().

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Đọc và ghi file
Đọc và ghi file
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION