Ngày 30 tháng 11 năm 2023 - Máy tính
Trong Java 7, đặc điểm try-with-resources
đã được giới thiệu để đảm bảo rằng các tài nguyên được tự động đóng lại sau khi sử dụng xong. Bất kỳ lớp nào triển khai giao diện java.lang.AutoCloseable
đều có thể coi là một tài nguyên và có thể sử dụng đặc điểm này. Bài viết này sẽ trình bày chi tiết cách sử dụng cũng như những điều cần lưu ý khi áp dụng đặc điểm này.
1 Sử dụng Tài nguyên Theo Cách Truyền Thống với try-finally
Trước Java 7, sau khi sử dụng tài nguyên, chúng ta cần phải thực hiện việc đóng tài nguyên bằng tay trong khối finally
.
Xem đoạn mã sau:
// src/test/java/TryWithResourcesTest#testJava6ReadFileWithFinallyBlock@TestpublicvoidtestJava6ReadFileWithFinallyBlock()throwsIOException{StringfilePath=this.getClass().getResource("test.txt").getPath();FileReaderfr=null;BufferedReaderbr=null;try{fr=newFileReader(filePath);br=newBufferedReader(fr);System.out.println(br.readLine());}finally{if(null!=fr){fr.close();}if(null!=br){br.close();}}}
Đoạn mã thử nghiệm trên cố gắng đọc một dòng nội dung từ tập tin test.txt
nằm trong thư mục resources
. Nó sử dụng các lớp luồng tệp tin FileReader
và BufferedReader
, và sau khi sử dụng xong, chúng được đóng lại trong khối finally
.
2 Vấn đề Khi Sử Dụng Tài Nguyên Theo Cách Truyền Thống
Cách tiếp cận truyền thống với try-finally
để sử dụng và đóng tài nguyên bằng tay có một số vấn đề:
- Rất dễ quên việc đóng tài nguyên, dẫn đến rò rỉ bộ nhớ;
- Khi có nhiều tài nguyên, cấu trúc lồng nhau làm giảm khả năng đọc mã;
- Nếu cả khối
try
và khốifinally
cùng phát sinh ngoại lệ, sẽ xảy ra hiện tượng che giấu ngoại lệ.
Dưới đây là giải thích chi tiết cho từng vấn đề này. Rất dễ quên việc đóng tài nguyên, dẫn đến rò rỉ bộ nhớ Việc giao trách nhiệm đóng tài nguyên cho lập trình viên rất dễ dẫn đến tình trạng quên. Nếu không đóng tài nguyên, nó sẽ vẫn bị giữ tham chiếu, ngăn cản bộ thu gom rác (garbage collector) thực hiện công việc của mình, cuối cùng có thể gây ra rò rỉ bộ nhớ.
Khi có nhiều tài nguyên, cấu trúc lồng nhau làm giảm khả năng đọc mã
Khi xử lý nhiều tài nguyên, có thể cần lồng nhiều khối try-finally
, làm giảm đáng kể sự rõ ràng và dễ hiểu của mã nguồn.
Nếu cả khối try
và khối finally
cùng phát sinh ngoại lệ, sẽ xảy ra hiện tượng che giấu ngoại lệ
Khi cả hai khối try
và finally
đều phát sinh ngoại lệ, ngoại lệ nào sẽ được ném ra cuối cùng?
Chúng ta có thể sửa đổi đoạn mã trên: Giả sử truyền một đường dẫn tệp không tồn tại và không kiểm tra giá trị null
trước khi đóng tài nguyên trong khối finally
.
Mã ví dụ:
// src/test/java/TryWithResourcesTest#testJava6ReadFileWithFinallyBlock@TestpublicvoidtestJava6ReadFileWithFinallyBlock()throwsIOException{StringfilePath="not-exists.txt";FileReaderfr=null;BufferedReaderbr=null;try{fr=newFileReader(filePath);br=newBufferedReader(fr);System.out.println(br.readLine());}finally{fr.close();br.close();}}
Khi chạy đoạn mã này, ngoại lệ xuất hiện như sau:
java.lang.NullPointerException: Cannot invoke "java.io.FileReader.close()" because "fr" is null
at TryWithResourcesTest.testJava6ReadFileWithFinallyBlock(TryWithResourcesTest.java:22)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Qua kiểm tra, do đường dẫn tệp không tồn tại, ngoại lệ FileNotFoundException
sẽ được ném ra trong khối try
. Sau đó, khi vào khối finally
, lệnh fr.close()
phát hiện rằng fr
chưa được khởi tạo và có giá trị null
, dẫn đến ngoại lệ NullPointerException
. Tuy nhiên, chỉ ngoại lệ NullPointerException
được hiển thị trực tiếp, còn FileNotFoundException
bị che giấu. Để lấy thông tin ngoại lệ bị che giấu, cần bắt ngoại lệ cuối cùng và gọi phương thức e.getSuppressed()
.
3 Java 7: Đóng Tài Nguyên Tự Động Với try-with-resources
Sử dụng đặc điểm try-with-resources
trong Java 7 giúp loại bỏ nhu cầu viết mã đóng tài nguyên thủ công. Sau khi khối try
hoàn thành, tất cả tài nguyên sẽ được tự động đóng lại.
Ví dụ mã:
// src/test/java/TryWithResourcesTest#testJava7ReadFileWithMultipleResources@TestpublicvoidtestJava7ReadFileWithMultipleResources()throwsIOException{StringfilePath=this.getClass().getResource("test.txt").getPath();try(FileReaderfr=newFileReader(filePath);BufferedReaderbr=newBufferedReader(fr)){System.out.println(br.readLine());}}
Như thấy ở ví dụ trên, việc khai báo và khởi tạo FileReader
và BufferedReader
được đặt trong phần ngoặc của khối try
, nhờ đó không cần đóng tài nguyên bằng tay nữa.
Đây thực chất là một cú pháp rút gọn (syntax sugar). Khi sử dụng đặc điểm này, trình biên dịch sẽ tự động thêm mã gọi phương thức close
để đóng tài nguyên.
Nếu chúng ta phân tích lại file .class
được tạo ra, chúng ta có thể thấy logic mà trình biên dịch đã thêm vào:
@TestpublicvoidtestJava7ReadFileWithMultipleResources()throwsIOException{StringfilePath=this.getClass().getResource("test.txt").getPath();FileReaderfr=newFileReader(filePath);try{BufferedReaderbr=newBufferedReader(fr);try{System.out.println(br.readLine());}catch(Throwablevar8){try{br.close();}catch(Throwablevar7){var8.addSuppressed(var7);}throwvar8;}br.close();}catch(Throwablevar9){try{fr.close();}catch(Throwablevar6){var9.addSuppressed(var6);}throwvar9;}fr.close();}
Nhìn thấy rằng trình biên dịch đã dùng cách viết truyền thống try-finally
để thêm mã đóng tài nguyên cho chúng ta. Thứ tự đóng tài nguyên là: tài nguyên được khai báo sớm hơn sẽ được đóng muộn hơn, và ngược lại. Ngoài ra, nếu xảy ra ngoại lệ khi đóng tài nguyên, ngoại lệ này sẽ bị che giấu, và ngoại lệ từ khối try-with-resources
sẽ được ném ra.
4 Lợi Ích Của Việc Sử Dụng try-with-resources
Trong Java 7
Các lợi ích khi chuyển sang sử dụng try-with-resources
bao gồm:
- Không cần đóng tài nguyên bằng tay, giảm thiểu nguy cơ rò rỉ bộ nhớ do quên đóng;
- Khối
try
có thể chứa một hoặc nhiều tài nguyên được phân tách bởi dấu chấm phẩy, giúp mã ngắn gọn và dễ đọc hơn; - Ngoại lệ từ khối
try-with-resources
sẽ được ném ra, trong khi ngoại lệ từ việc đóng tài nguyên sẽ bị che giấu (cơ chế này khác với cách xử lý củatry-finally
).
5 Nâng Cấp Đặc Điểm try-with-resources
Từ Java 9
Từ ví dụ trên, ta thấy rằng trong Java 7, việc khai báo và khởi tạo tài nguyên phải diễn ra bên trong khối try-with-resources
.
Tuy nhiên, từ Java 9 trở đi, tài nguyên có thể được khai báo và khởi tạo bên ngoài khối try-with-resources
, chỉ cần đưa biến tài nguyên vào khối try-with-resources
là đủ.
Ví dụ mã:
// src/test/java/TryWithResourcesTest#testJava9ReadFileWithMultipleResources@TestpublicvoidtestJava9ReadFileWithMultipleResources()throwsIOException{StringfilePath=this.getClass().getResource("test.txt").getPath();FileReaderfr=newFileReader(filePath);BufferedReaderbr=newBufferedReader(fr);try(fr;br){System.out.println(br.readLine());}}
6 Triển Khai Tài Nguyên Tuân Thủ Giao Diện AutoCloseable
Bài viết đã nhắc đến cổng game trực tuyến r88 rằng không chỉ các tài nguyên mặc định của Java (như InputStream
, OutputStream
, hay java.sql.Connection
) có thể sử dụng đặc điểm try-with-resources
, mà bất kỳ tài nguyên nào triển khai giao diện AutoCloseable
đều có thể sử dụng.
Dưới đây là một ví dụ triển khai tài nguyên tuân thủ AutoCloseable
và sử dụng nó với try-with-resources
.
Ví dụ mã:
staticclass MyResourceimplementsAutoCloseable{@Overridepublicvoidclose(){System.out.println("my resource closed!");}publicvoiddoSomething(){System.out.println("do something");}}// src/test/java/TryWithResourcesTest#testJava7CustomResourceUsage@TestpublicvoidtestJava7CustomResourceUsage(){try(MyResourcemyResource=newMyResource()){myResource.doSomething();}}
Như vậy, để triển khai giao diện AutoCloseable
, chỉ cần triển khai phương thức close
. Việc sử dụng tài nguyên tùy chỉnh với try-with-resources
không khác gì so với tài nguyên mặc định.
Tóm lại, bài viết đã trình bày cách quản lý tài nguyên trước khi có đặc điểm try-with-resources
, cách sử dụng đặc điểm này và lợi ích của nó, cũng như nâng cấp từ Java 9 và cách triển khai tài nguyên tùy chỉnh tuân thủ AutoCloseable
.
Tất cả các ví dụ mã trong bài đã được đăng tải trên GitHub cá nhân của tôi, mọi người có thể theo dõi hoặc Fork.
[1] Tạo và Hủy Đối Tượng: Ưu Tiên
try-with-resources
Thay Vìtry-finally
| Effective Java (Ấn Bản 3), bởi Joshua Bloch [2] Câu Lệnhtry-with-resources
(Hướng Dẫn Java™) | Oracle - docs.oracle.com [3] Thay Đổi Ngôn Ngữ Java Cho Java SE 9: Các Câu Lệnhtry-with-resources
Ngắn Gọn Hơn | Oracle - docs.oracle.com [4] Java Try With Resources | Jakob Jenkov - jenkov.com [5]try-with-resource
Có An Toàn Không Khi Khai Báo Nhiều Tài Nguyên Hiệu Quả? | Stackoverflow - stackoverflow.com [6] Ví Dụ Java try-with-resources | Mkyong - mkyong.com