Làm thế nào để xử lí ngoại lệ sau khi đã biết thông tin về các loại ngoại lệ có thể phát sinh từ các phương thức ta dùng đến trong chương trình? Có hai lựa chọn, một là giải quyết tại chỗ, hai là tránh né trách nhiệm. Thực ra lựa chọn thứ hai không hẳn là né được hoàn toàn, nhưng ta sẽ trình bày chi tiết về lựa chọn này sau. Trước hết, ta nói về cách xử lí ngoại lệ tại chỗ.
Để xử lý các ngoại lệ có thể được ném ra từ một đoạn mã, ta bọc đoạn mã đó trong một khối try/catch. Chương trình trong Hình 11.3 sau khi được sửa như trong Hình 11.5 thì biên dịch và chạy thành công.
Khối try/catch gồm một khối try chứa phần mã có thể phát sinh ngoại lệ và ngay sau đó là một khối catch với nhiệm ‘bắt’ ngoại lệ được ném từ trong khối try và xử lí sự cố đó (có thể có vài khối catch theo sau một khối try, ta sẽ nói đến vấn đề này sau). Nội dung của khối catch tùy vào việc ta muốn làm gì khi loại sự cố cụ thể đó xảy ra. Ví dụ, trong Hình 11.5, khối catch chỉ làm một việc đơn giản là gọi phương thức printStackTrace() của ngoại lệ vừa bắt được để in ra màn hình thông tin về dấu vết của ngoại lệ đó trong ngăn xếp các lời gọi phương thức (stack trace). Đây là hoạt động xử lý ngoại lệ thường dùng trong khi đang tìm lỗi của chương trình.
Ngoại lệ là đối tượng
Cái gọi là ngoại lệ mà nơi ném nơi bắt đó thực chất là cái gì trong ngôn ngữ Java? Cũng như nhiều thứ khác trong chương trình Java, mỗi ngoại lệ là một đối tượng của cây phả hệ Exception. Nhớ lại kiến thức về đa hình, ta lưu ý rằng mỗi đối tượng ngoại lệ có thể là thực thể của một lớp con của Exception. Hình 11.6 mô tả một phần của cây phả hệ Exception với FileNotFoundException và ArithmeticException là những loại ngoại lệ ta đã gặp trong các ví dụ của chương này.
Do mỗi ngoại lệ là một đối tượng, cái được ‘bắt’ trong mỗi khối catch là một đối tượng, trong đó đối số của catch là tham chiếu tới đối tượng đó. Khối catch trong Hình 11.5 có tham số e là tham chiếu được khai báo thuộc kiểu FileNotFoundException.
Mội khối catch khai báo tham số thuộc kiểu ngoại lệ nào thì sẽ bắt được các đối tượng thuộc kiểu ngoại lệ đó. Cũng theo nguyên tắc thừa kế và đa hình rằng các đối tượng thuộc lớp con cũng có thể được coi như các đối tượng thuộc kiểu lớp cha. Do đó, một khối catch khai báo tham số kiểu lớp cha thì cũng bắt được đối tượng ngoại lệ thuộc các lớp con của kiểu đó. Ví dụ khối catch(Exception e) {…} bắt được các đối tượng thuộc các lớp Exception, IOException, cũng như FileNotFoundException.
Học lập trình Web trong vòng 6 tháng, đảm bảo 100% công việc đầu ra.
KHỐI try/catch
Mục trước đã giới thiệu về việc dùng khối try/catch để bắt và xử lý ngoại lệ. Mục này trình bày kĩ hơn về cấu trúc và cơ chế hoạt động của khối try/catch.
Bắt nhiều ngoại lệ
Như ta đã thấy, ví dụ Hình 11.1 khi chạy có thể phát sinh hai loại ngoại lệ InputMismatchException hay ArithmeticException. Để xử lý hai ngoại lệ này, ta cũng dùng một khối try/catch tương tự như đã làm trong Hình 11.5. Nhưng lần này ta dùng hai khối catch, mỗi khối dành để xử lý một loại ngoại lệ. Mỗi khối try/catch chỉ có một khối try, tiếp theo là một hoặc vài khối catch.
Khi một ngoại lệ xảy ra, trình biên dịch tìm một khối catch phù hợp trong các khối catch đi kèm. Trình tự tìm là lần lượt từ khối thứ nhất đến khối cuối cùng, khối catch đầu tiên bắt được ngoại lệ đó sẽ được thực thi.
Hoạt động của khối try/catch
Khi ta chạy một lệnh/phương thức có thể sinh ngoại lệ, một trong hai trường hợp xảy ra: (1) phương thức được gọi thành công; (2) phương thức được gọi ném ngoại lệ và khối catch bắt được ngoại lệ đó, và (3) phương thức được gọi ném ngoại lệ nhưng khối catch không bắt được ngoại lệ đó. Luồng điểu khiển trong khối try/catch trong các trường hợp đó cụ thể như sau:
(1) Phương thức được gọi thành công, và khối try được thực thi đầy đủ cho đến lệnh cuối cùng, còn khối catch bị bỏ qua vì không có ngoại lệ nào phải xử lý. Sau khi khối try chạy xong, lệnh đằng sau catch (nghĩa là nằm ngay sau khối try/catch) sẽ chạy.
(2) Phương thức được gọi ném ngoại lệ và khối catch bắt được ngoại lệ đó. Các lệnh trong khối try ở sau lệnh phát sinh ngoại lệ bị bỏ qua, điều khiển chuyển tới khối catch, sau khi khối catch thực thi xong, phần còn lại của phương thức tiếp tục chạy.
(3) Phương thức được gọi ném ngoại lệ nhưng khối catch không bắt được ngoại lệ đó. Nếu không dùng khối finally mà ta nói đến ở mục sau, điều khiển sẽ nhảy ra khỏi chương trình, bỏ qua phần còn lại của phương thức kể từ sau lệnh phát sinh ngoại lệ và ra khỏi phương thức hiện tại. Điều khiển sẽ quay về nơi gọi phương thức hiện tại hoặc chương trình dừng do lỗi run-time.
Khối finally – những việc dù thế nào cũng phải làm
Phần try và phần catch trong khối try/catch là những phần bắt buộc phải có. Ngoài ra, ta còn có thể lắp một phần có tên finally vào làm phần cuối cùng của khối try/catch.
Một khối finally là nơi ta đặt các đoạn mã phải được thực thi bất kể ngoại lệ có xảy ra hay không.
Ta lấy một ví dụ minh họa. Giả sử ta cần luộc trứng trong lò vi sóng. Nếu có sự cố xảy ra, chẳng hạn trứng bị nổ, ta phải tắt lò. Nếu trứng luộc thành công, ta cũng tắt lò. Tóm lại, dù chuyện gì xảy ra thì ta cũng đều phải tắt lò.
Nếu không dùng khối finally, ta phải gọi turnOvenOff() ở cả khối try lẫn khối catch, nhưng kết quả là vẫn không thực hiện được nhiệm vụ đóng file nếu kết cục lại xảy ra theo trường hợp (3) đã nói đến, khi điều khiển chương trình bỏ qua cả khối catch để ra ngoài.
Với khối finally, trong bất kể tình huống nào, luồng điều khiển cũng phải chạy qua khối lệnh đó. Khi ngoại lệ bị ném ra mà không có khối catch nào bắt được, khối finally cũng chạy trước khi luồng điều kiển ra khỏi phương thức. Ngay cả khi có lệnh return trong khối try hoặc một khối catch, khối finally cũng được thực thi trước khi quay lại chạy lệnh return đó.
Với đặc điểm đó, khối finally cho phép ta đặt các đoạn mã dọn dẹp tại một nơi thay vì phải lặp lại nó tại tất cả các điểm mà điều khiển chương trình có thể thoát ra khỏi phương thức.
Lưu ý rằng, về mặt cú pháp, ta không thể chèn mã vào giữa các phần try, catch, và finally trong một khối try/catch; khối try thì bắt buộc phải có, nhưng các khối catch và finally thì không; tuy nhiên, sau một khối try phải có ít nhất một khối catch hoặc finally.
Thứ tự cho các khối catch
Ngoại lệ cũng là các đối tượng nên có tính đa hình, và một khối catch dành cho ngoại lệ lớp cha cũng bắt được ngoại lệ lớp con. Ví dụ các khối catch sau đều bắt được ngoại lệ loại InputMismatchException:
catch(InputMismatchException e) {…} chỉ bắt InputMismatchException, catch(IOException e) {…} bắt tất cả các IOException, trong đó có InputMismatchException
catch(Exception e) {…} bắt tất cả các Exception, trong đó có các IOException.
Có thể hình dung catch(Exception e) là một cái rổ to nhất và hứng được các loại đồ vật với nhiều kích thước hình dạng khác nhau, catch(IOException e) là cái rổ nhỏ hơn chút nên hứng được ít loại đồ vật hơn, còn catch (InputMismatchException e) là cái rổ nhỏ nhất và chỉ hứng vừa một loại đồ vật. Ta có thể chỉ dùng một cái rổ to nhất – khối catch bắt loại ngoại lệ tổng quát nhất – để bắt tất cả các ngoại lệ và xử lý một thể. Tuy nhiên, nếu ta muốn xử lý tùy theo các ngoại lệ thuộc loại khác nhau thì nên dùng các khối catch khác nhau trong một khối try/catch.
Vậy các khối catch đó nên được để theo thứ tự nào? Nhớ lại rằng khi một ngoại lệ được ném ra từ bên trong khối try, theo thứ tự từ trên xuống dưới, khối catch nào bắt được ngoại lệ đó thì sẽ được chạy. Do đó, nếu cái rổ to được thử hứng trước cái rổ nhỏ hơn, nghĩa là khối catch cho lớp cha được đặt trước khối catch dành cho lớp con, thì cái rổ to sẽ hứng được ngay còn cái rổ nhỏ hơn sẽ không bao giờ đến lượt mình hứng được cái gì. Vì lí do đó, trình biên dịch yêu cầu khối catch dành cho lớp ngoại lệ tổng quát hơn bao giờ cũng phải đặt sau khối catch dành cho lớp ngoại lệ chuyên biệt hơn. Trình biên dịch sẽ báo lỗi nếu ta không tuân theo quy tắc này.