Java Bài 39: Exception Tập 3 – Try Catch Và Hơn Thế Nữa

Posted by

Chào mừng các bạn đã đến với bài học Java số 39, bài học về Exception (phần tiếp theo). Đây là bài học trong chuỗi bài về lập trình ngôn ngữ Java của Yellow Code Books.

Thông qua hai bài viết về Exception, bạn đã phần nào yên tâm hơn cho “số phận” của các dòng code mà bạn tạo ra rồi đúng không nào. Sự thật là với việc sử dụng tốt try catch mà hai bài học trước mình đã nói rất kỹ, thì có thể nói ứng dụng của bạn sẽ trở nên rất mạnh mẽ, an toàn, và cũng khá thông minh khi có thể thông báo kịp thời các trường hợp lỗi cho user.

Tuy nhiên, nếu đã nói về Exception thì phải nói cho tới. Đừng lấp lửng nửa vời mà lỡ mất các chức năng đặc sắc mà công cụ này mang đến. Hôm nay chúng ta tiếp tục đào sâu về Exception khi nói về try catch với finally, try catch với resource, và các phương thức hữu ích của lớp Exception.

Try Catch Với Finally

Mình nhắc lại một chút từ các bài học trước, rằng khi làm quen với cách thức bắt Exception thông qua try catch, thì try giúp bao bọc đoạn code có khả năng gây ra lỗi, còn catch sẽ giúp xử lý “hậu quả” khi mà code trên đã chính thức bị lỗi.

Tuy nhiên nhiêu đó chưa đủ. Trong Java, đôi khi chúng ta có sử dụng đến một số tài nguyên của hệ thống. Tài nguyên này chính là các resource có khả năng dùng chung giữa các ứng dụng. Đó có thể đơn giản chỉ là một file nào đó. Và bởi vì đây là các resource dùng chung, nên nếu có một tình huống khi ứng dụng của bạn đang mở một resource ra xem, nhưng chẳng may lại gây ra một Exception nào đó trong quá trình này, khi đó ứng dụng thông báo lỗi, và… xong… vô tâm không đóng lại cái resource ấy. Trường hợp này, hệ thống đang tưởng rằng resource vẫn được quản lý bởi ứng dụng của bạn, nhưng thực chất là ứng dụng của bạn đã chết từ lâu rồi.

Qua đó bạn có thể thấy rằng, trong lập trình bạn không nên để ứng dụng nắm giữ bất kỳ tài nguyên nào cho riêng bạn, bạn nên đóng nó lại sau khi dùng xong. Bạn sẽ hiểu rõ điều này khi làm quen với kiến thức về Input/Output ở các bài tiếp theo. Từ tình huống trên mà finally ra đời. Finally sẽ giúp chúng ta dọn dẹp các tàn dư từ try catch để lại, trong bài học hôm nay nhiệm vụ chính của nó là đóng các tài nguyên lại khi có Exception xảy ra. Trước hết mời bạn xem cú pháp sau.

try {
	// Các dòng code có khả năng gây ra Exceptipn
} catch (ExceptionClass1 e1) {
	// Bắt các EceptionClass1
} catch (ExceptionClass2 e2) {
	// Bắt các EceptionClass2
} catch (ExceptionClass3 e3) {
	// Bắt các EceptionClass3
} finally {
	// Giải phóng các tài nguyên đang sử dụng
}

Qua cú pháp trên, bạn có thể thấy finally sẽ đi kèm với try catch. Nếu không có try catch sẽ không có finally. Nhưng dù try có đi kèm với bao nhiêu catch đi chăng nữa, thì chỉ cần có một finally để dọn dẹp những gì bạn đã dùng là đủ.

Dưới đây là một vài ý liên quan đến finally, mình hi vọng các mục này sẽ giúp bạn có cái nhìn rõ nhất về finally này.

Chỉ Cần Có Try – Finally (Mà Không Cần Catch) Cũng Được

Bạn có thể hiểu rằng finally trong trường hợp này được dùng để dọn dẹp cái gì đó mà không nhất thiết phải có Exception xảy ra. Vì nhu cầu sử dụng finally khi này rất ít nên ví dụ dưới đây của mình sẽ không có tính thực tế lắm, chủ yếu giúp bạn hiểu thôi.

try {
	System.out.println("Bài toán thực hiện phép chia:");
	int ketQua = 10 / 2;
	System.out.println("10 chia 2 bằng: " + ketQua);
} finally {
	System.out.println("Kết thúc chương trình");
}

Chắc bạn cũng đoán biết kết quả rồi. Rằng cả 3 dòng in ra console ở trên đều thực hiện một cách trôi chảy.

Exception tập 3 - try catch với finally

Bạn Có Thể Không Cần Đến Finally, Nhưng Đã Có Finally Thì Code Trong Finally Luôn Được Gọi Đến Cuối Cùng

Ví dụ về try catch không cần đến finally thì bạn đã biết thông qua bài học trước. Nhưng nếu bạn có khai báo finally cho try catch, thì các câu lệnh trong khối finally sẽ được thực thi cuối cùng, sau khi ứng dụng đã thực thi các câu lệnh không gây ra Exceptiontry, rồi đến các câu lệnh trong khối catch tương ứng nếu có, cuối cùng mới đến finally.

try {
	System.out.println("Bài toán thực hiện phép chia:");
	int ketQua = 10 / 0;
	System.out.println("10 chia 0 bằng: " + ketQua);
} catch (ArithmeticException e) {
	System.out.println("Phép chia không thực hiện được");
} finally {
	System.out.println("Kết thúc chương trình");
}

Với các dòng code này thì console sẽ in ra như sau. Và có lẽ mình cũng không cần giải thích gì nhiều.

Exception tập 3 - try catch với finally

Trong Finally Vẫn Có Thể Có Try Catch Trong Đó

Nếu như trong try hay catch ta đều có thể lồng thêm try catch vào. Thì finally cũng cho phép như thế. Mặc dù trông có vẻ phức tạp và hơi thiếu an toàn, nhưng đời không có gì hoàn hảo cả, đã bắt lỗi rồi vẫn có thể xảy ra lỗi ở quá trình bắt lỗi là chuyện thường tình.

Bạn sẽ được làm quen với finally và cả try catch bên trong finally qua bài thực hành dưới đây.

Thực Hành Xây Dựng Try Catch Với Finally

Bài thực hành này giúp chúng ta làm quen với việc sử dụng try catchfinally một cách thực tế hơn. Chúng ta sẽ kế thừa code từ bài thực hành hôm trước.

Vì bài thực hành hôm trước chúng ta chỉ mới làm cách nào để bắt Exception hiệu quả thôi. Thực tế bạn đang sử dụng FileOutputStream để mở một file từ hệ thống và thao tác với file đó. File này chính là tài nguyên có khả năng dùng chung bởi các chương trình khác. Như mình có nói, nếu bạn sử dụng file này mà lỡ có chuyện gì xảy ra mà bạn không giải phóng việc sở hữu đó, thì khi đó bạn đang “ích kỷ” chỉ giữ file đó cho riêng bạn, các ứng dụng khác không thể đọc được file này.

Đó là lý do vì sao chúng ta cần xây dựng finally để giải phóng cái tài nguyên này như sau.

Đầu tiên, chúng ta cần phải code các dòng có gây ra Checked Exception như sau.

FileOutputStream outputStream = null;
outputStream = new FileOutputStream("E://file.txt");
outputStream.write(65);
outputStream.close();

Và như với việc thực hành ở bài trước, bạn đã biết cách thức dễ nhất để hệ thống tự thêm vào các try catch cho các dòng đang bị báo lỗi. Sau khi áp dụng các sự gợi ý từ Eclipse (xem ở bài trước), code của chúng ta trông khá là đẹp như sau.

FileOutputStream outputStream = null;
try {
	outputStream = new FileOutputStream("E://file.txt");
	outputStream.write(65);
	outputStream.close();
} catch (FileNotFoundException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
} catch (IOException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}

Vấn đề của code trên đây là gì? Bạn biết rằng file có cái tên file.txt được xem như một tài nguyên dùng chung. Có nghĩa là trong lúc bạn mở file này ra để ghi vào giá trị 65, trong lúc đó, có thể có một chương trình khác cũng đang muốn thao tác với file này. Và vấn đề xảy ra là, ngỡ như trong quá trình bạn ghi vào file mà có bất cứ lỗi nào xảy ra, khiến cho chương trình bị bẻ luồng thực thi vào các dòng code bên trong khối catch bên dưới. Thì câu lệnh outputStream.close() sẽ không có cơ hội thực thi. File mà bạn thao tác sẽ vẫn được hệ thống xem là đang được sử dụng. Và như vậy ứng dụng khác phải chờ hoài mà không thấy ứng dụng của bạn trả lại quyền sử dụng file này.

Các vấn đề thao tác với file chúng ta sẽ làm quen ở các bài học sau. Còn bây giờ, hãy đảm bảo rằng file mà bạn đang dùng luôn được đóng lại một cách an toàn. Trả lại quyền sử dụng tài nguyên cho các chương trình khác. Thật đơn giản, chúng ta cùng thêm finally và đoạn code đóng file này lại trong khối này là xong. Như hình sau.

Screen Shot 2018-05-24 at 16.44.03

Ồ nhưng nhìn xem, code để vào trong finally vẫn thuộc thẩm quyền của Checked Exception. Rất tốt, chúng ta cũng nên cần một lần try catch nữa. Code cuối cùng sẽ như sau.

FileOutputStream outputStream = null;
try {
	outputStream = new FileOutputStream("E://file.txt");
	outputStream.write(65);
	outputStream.close();
} catch (FileNotFoundException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
} catch (IOException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
} finally {
	try {
		outputStream.close();
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

Try Catch Với Resource

Kiểu try catch này xuất hiện từ Java 7. Và đây là một kiểu nâng cấp so với try catch với finally mà bạn vừa làm quen trên kia.

Cụ thể là, thông qua bài thực hành trên đây, bạn đã thấy tầm quan trọng khi giải phóng các tài nguyên của hệ thống rồi đúng không nào. Và bạn cũng đã thấy cách đóng file lại trong khối finally rồi đó. Nhưng nếu chú ý kỹ bài thực hành trên thì, chúng ta thấy khối finally sẽ được gọi đến dù cho câu lệnh khởi tạo FileInputStream có thực hiện hoàn hảo hay không, do đó việc đóng file trong khối finally hoàn toàn có thể gây ra một Exception khác. Và cũng vì lý do này mà chúng ta mới có thêm try catch bên trong finally.

Nhưng việc thực hiện thủ công finallytry catch bên trong finally như ví dụ trên cũng có cái dở, là nếu có Exception xảy ra với các code trong try và cả với code trong finally, thì các dòng lỗi in ra sẽ loạn cả lên.

Chính vì vậy mà Java 7 đã cho chúng ta một công cụ mới với cái tên try catch với resource (Try With Resources). Cú pháp của nó như sau.

try(// Khởi tạo các tài nguyên) {
	// Sử dụng các tài nguyên đã khởi tạo
} catch (ExceptionClass1 e1) {
	// Bắt các EceptionClass1
} catch (ExceptionClass2 e2) {
	// Bắt các EceptionClass2
} catch (ExceptionClass3 e3) {
	// Bắt các EceptionClass3
}

Như bạn thấy, cách try catch này không còn đi kèm với finally nữa. Vì các tài nguyên được khởi tạo bên trong cặp ngoặc đơn của try như trên giờ đây đã có thể tự biết cách đóng lại khi kết thúc khối lệnh try.

Trước khi đi vào bài thực hành chi tiết, chúng ta xem qua một vài ý chính của try catch với resource để giúp chúng ta hiểu rõ hơn.

Không Phải Tài Nguyên Nào Cũng Dùng Được Trong Try Catch Với Resource

Chỉ có các đối tượng có hiện thực interface java.lang.AutoCloseable, như FileOutputStream ở ví dụ trên kia, mới có thể dùng trong try catch với resource.

Trong Quá Trình Đóng Các Tài Nguyên Hệ Thống, Nếu Xảy Ra Lỗi Thì Sẽ Không Có Exception Của Quá Trình Này

Ý này có nghĩa là, nếu với cách sử dụng try catch với finally trên kia, giả sử khi bạn khởi tạo FileOutputStream bị lỗi, Exception của sự khởi tạo này được tung ra, chương trình bị bẻ luồng vào catch tương ứng, điều này thì bạn biết quá rõ rồi. Nhưng sau đó các code bên trong khối finally thực hiện, nếu việc đóng FileOutputStream thông qua câu lệnh outputStream.close() vẫn gây ra lỗi, Exception của sự đóng này lại được tung ra, và chương trình lại bị bẻ luồng vào catch bên trong finally này.

Nhưng với try catch với resource thì lại khác. Nếu cái sự khởi tạo FileOutputStream bị lỗi, chỉ có Exception của try được tung ra và chương trình chỉ bị bẻ luồng vào catch tương ứng. Việc đóng FileOutputStream lại nếu có lỗi hay không thì chương trình cũng không tung ra bất kỳ Exception nào nữa cả. Đó là sự khác biệt cơ bản giữa hai try catch mà chúng ta đang nói đến của bài hôm nay.

Có Thể Khởi Tạo Nhiều Tài Nguyên Bên Trong Khối Try

Có nhiều khi chúng ta muốn đảm bảo nhiều hơn một tài nguyên bên trong khối try, chúng ta chỉ việc khởi tạo các tài nguyên này và cách nhau bằng dấu (;) mà thôi. Chi tiết về ý này sẽ được mình thể hiện ở các bài thực hành sau đây.

Thực Hành Xây Dựng Try Catch Với Resource

Chúng ta sẽ sửa code của bài thực hành trên kia bằng kiểu try catch với resource.

Rất dễ và nhanh chóng, bạn chỉ cần đưa code khởi tạo tài nguyên vào trong cặp ngoặc đơn của try, và bỏ khối finally (và tất cả các code đóng tài nguyên lại) của bài thực hành trên là được thôi.

try(FileOutputStream outputStream = new FileOutputStream("E://file.txt")) {
	outputStream.write(65);
} catch (FileNotFoundException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
} catch (IOException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}

Thực Hành Xây Dựng Try Catch Với Nhiều Resource

Như đã hứa trên kia, bài thực hành này sẽ giúp bạn hiểu rõ try catch với việc khởi tạo nhiều hơn một tài nguyên bên trong cặp ngoặc đơn của try như thế nào.

Có thể bạn chưa hiểu hết các câu lệnh sau, nhưng bạn cứ thực hành đi, sau này chúng ta sẽ nói rõ hơn các câu lệnh này ở các bài học liên quan. Các câu lệnh dưới đây sẽ đọc file nào đó do bạn chỉ định. Và do các dòng code sau có chứa hai tài nguyên có hiện thực interfce java.lang.AutoCloseable, chúng là FileInputStreamBufferedInputStream, nên chúng ta sẽ khởi tạo chúng bên trong cặp ngoặc đơn của try như sau. Hai tài nguyên được khởi tạo này sẽ được tự động đóng lại sau khi khối lệnh try catch kết thúc.

try(FileInputStream inputStream = new FileInputStream("E://file.txt");
		BufferedInputStream bufferInputStream = new BufferedInputStream(inputStream)) {
	int data = bufferInputStream.read();
	while(data != -1) {
		System.out.print((char) data);
		data = bufferInputStream.read();
	}
} catch (FileNotFoundException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
} catch (IOException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}

Vài Phương Thức Hữu Ích Của Lớp Exception

Mục này là phần nói thêm của mình trong bài hôm nay. Tuy mình chỉ nêu vài phương thức thôi, nhưng mình hi vọng bạn sẽ hiểu rõ hơn về cách sử dụng Exception và ứng dụng nó vào một số hoàn cảnh cụ thể nào đó.

getMessage()

Phương thức này trả về một String mô tả về Exception vừa xảy ra. Bạn có thể thử nghiệm bằng cách gọi đến các dòng code sau. Mình cố tình làm cho các dòng code gây ra Exception.

try(FileOutputStream outputStream = new FileOutputStream("E://file_not_found.txt")) {
	outputStream.write(65);
} catch (FileNotFoundException e) {
	System.out.println(e.getMessage());
} catch (IOException e) {
	System.out.println(e.getMessage());
}

Và đây là kết quả mà phương thức getMessage() này trả về.

Exception tập 3 - Phương thức getMessage()

toString()

Trả về tên của lớp Exception đang tung ra lỗi, kèm với nội dung của phương thức getMessage() trên kia. Bạn có thể thử.

try(FileOutputStream outputStream = new FileOutputStream("E://file_not_found.txt")) {
	outputStream.write(65);
} catch (FileNotFoundException e) {
	System.out.println(e.toString());
} catch (IOException e) {
	System.out.println(e.toString());
}

Và đây là kết quả.

Exception tập 3 - Phương thức toString()

printStackTrace()

Đây là một phương thức giúp bạn in ra nội dung của phương thức toString() trên kia, kèm với các dòng log dạng Stack Trace nữa.

Nào chúng ta cùng thử nghiệm.

try(FileOutputStream outputStream = new FileOutputStream("E://file_not_found.txt")) {
	outputStream.write(65);
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}

Và cái kết.

Exception tập 3 - Phương thức printStackTrace()

Các bạn vừa xem xong phần 3 của loạt bài về Exception. Bài học hôm nay giúp chúng ta đi sâu hơn về cách sử dụng try catch để bắt các Exception và giải phóng tài nguyên. Chúng ta còn một phần nữa về kiến thức Exception này, bạn đón xem nhé.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:

Đánh giá 5 sao bên dưới mỗi bài nếu thấy thích.
Comment bên dưới mỗi bài nếu có thắc mắc.
Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.

Bài Kế Tiếp

Chúng ta sẽ tìm hiểu về Throws ExceptionCustom Exception.

Advertisements
Rating: 5.0/5. From 8 votes.
Please wait...

6 comments

  1. Cảm ơn anh, bài viết rất hữu ích. Hi vọng anh sẽ tiếp tục ra tiếp những bài mới dù biết rằng anh cũng đang rất bận với công việc. Cảm ơn anh rất nhiều !!!

  2. Bạn ơi sao mấy phần ví dụ getMessenge() hay printStackTrace(), toString() mình chạy ko ra messenge nhỉ (mặc dù ko có lỗi), mình dùng netBean,nó chạy ko báo lỗi nhưng vẫn báo build Sucess, thanks bạn!

    1. Chào bạn. Lâu quá mình không dùng Netbeans nên quên console của nó sẽ được show ra ở đâu rồi. Vấn đề của bạn là build success thật, nhưng khi chạy ở runtime thì sẽ bị báo lỗi, mà có lẽ Netbeans đã có show ra lỗi đó, nhưng cửa sổ console của Netbeans bị ẩn đi nên bạn chẳng thấy gì cả. Bạn thử xem có cửa sổ nào liên quan đến chữ Output đang được trông thấy trên Netbeans của bạn không, và xem kết quả in ra trong cửa sổ đó nhé.

  3. anh ơi anh dạy thêm các phần như thao tác với file, hoặc là ArrayList hay nói chung là những công cụ hay ho trong java đi anh

Gửi phản hồi