Ném và né ngoại lệ

Xử lí ngoại lệ và Debug

NÉM NGOẠI LỆ

Nếu mã chương trình của ta phải bắt ngoại lệ, thì ta cần biết mã của ai ném nó? Các ví dụ ta đã dùng từ đầu chương đều nói về các tình huống mà ngoại lệ được ném từ bên trong một hàm trong thư viện. Ta gọi một phương thức có khai báo một loại ngoại lệ, và phương thức đó ném ngoại lệ trở lại đoạn chương trình gọi nó.

Trong thực tế, ta có thể phải viết cả mã ném ngoại lệ cũng như mã xử lý ngoại lệ. Vấn đề không phải ở chỗ ai viết cái gì, mà là biết rằng phương thức nào ném ngoại lệ và phương thức nào bắt nó.

Nếu viết một phương thức có thể ném một ngoại lệ, ta phải làm hai việc: (1) tuyên bố tại dòng khai báo phương thức rằng nó có thể ném loại ngoại lệ đó (dùng từ khóa throws); (2) tạo một ngoại lệ và ném nó (bằng lệnh throw) tại tình huống thích hợp trong nội dung phương thức.

Hình 11.10: Ném và bắt ngoại lệ

NÉ NGOẠI LỆ

Đôi khi, ta có một phương thức dùng đến những lời gọi hàm có thể phát sinh ngoại lệ, nhưng ta không muốn xử lý một ngoại lệ tại phương thức đó. Khi đó, ta có thể ‘né’ bằng cách khai báo throws cho loại ngoại lệ đó khi viết định nghĩa phương thức. Kết quả của khai báo throws đối với một loại ngoại lệ là: nếu có một ngoại lệ thuộc loại đó được ném ra bởi một lệnh nằm trong phương thức, nó không được ‘đỡ’ mà sẽ ‘rơi’ ra ngoài phương thức, tới nơi gọi phương thức (caller).

Hình 11.5: Xử lý ngoại lệ với khối try/catch (Bài xử lý ngoại lệ)

Hình 11.11: Né ngoại lệ để nơi gọi xử lý

Ta còn nhớ ví dụ trong Hình 11.5, tại đó phương thức write() gọi đến new PrintWriter() bắt và xử lý ngoại lệ do new PrintWriter() ném ra. Bây giờ ta không muốn bắt và xử lý ngoại lệ ngay tại write() mà để cho nơi gọi write xử lý. Ta bỏ khối try/catch tại write() và thay bằng khai báo throws, sửa FileWriter thành như trong Hình 11.11. Khi đó, việc bắt và xử lý ngoại lệ trở thành trách niệm của nơi gọi write(), như phương thức main trong Hình 11.11.

Có thể hình dung cơ chế ném, bắt, né như thế này: Ngoại lệ như một đồ vật được ném ra từ phương thức đang chạy – nó nằm trên đỉnh ngăn xếp của các lời gọi phương thức (method call stack). Nó sẽ rơi từ trên xuống. Trong các phương thức đang nằm trong ngăn xếp, phương thức nào né với khai báo throws phù hợp sẽ giống như giương ra một cái lỗ vừa với ngoại lệ để nó lọt qua và rơi tiếp xuống dưới. Phương thức nào bắt với khối try/catch phù hợp giống như giương ra một cái rổ hứng lấy ngoại lệ, nó được bắt để xử lý tại đây nên không rơi xuống tiếp nữa. Tóm lại, sau khi một ngoại lệ được ném, nó rơi từ trên xuống, lọt qua các phương thức có khai báo throws (tính cả phương thức ném nó), và bị giữ lại tại phương thức đầu tiên có khai báo catch bắt được nó. Trong quá trình rơi, nếu nó rơi vào một phương thức không có khai báo throws phù hợp hay khối try/catch phù hợp, nghĩa là phương thức đó không cho nó lọt qua, cũng không lấy rổ hứng, thì trình biên dịch sẽ báo lỗi.

Hình 11.4: Thông tin về ngoại lệ tại đặc tả phương thức (Bài giới thiệu về ngoại lệ)

Hình 11.12 minh họa quá trình rơi của một ngoại lệ FileNotFoundException với cài đặt như trong Hình 11.11. Trong đó, để đối phó với FileNotFoundException, WriteToFile.main có khối try/catch, FileWriter khai báo throws, và ta còn nhớ trong Hình 11.4, hàm khởi tạo PrintWriter(File) cũng khai báo throws đối với loại ngoại lệ này. Với trình tự main gọi write, còn write gọi hàm khởi tạo PrintWriter, ngoại lệ được ném ra từ trong PrintWriter, lọt qua write, rơi xuống main và được bắt tại đó. Các phương thức được đại diện bởi hình chữ nhật có cạnh là những đường đứt đoạn là những phương thức đã kết thúc do ngoại lệ.

Việc né ngoại lệ thực ra chỉ trì hoãn việc xử lý ngoại lệ chứ không tránh được hoàn toàn. Nếu nơi cuối cùng trong chương trình là hàm main cũng né, ngoại lệ sẽ không được xử lý ở bất cứ khâu nào. Trong trường hợp đó, tuy trình biên dịch sẽ cho qua, nhưng khi chạy chương trình, nếu có ngoại lệ xảy ra, máy ảo Java sẽ ngắt chương trình y như những trường hợp ngoại lệ không được xử lý khác.

Tổng kết lại, quy tắc hành xử mỗi khi gọi một phương thức có thể phát sinh ngoại lệ là: bắt hoặc né. Ta bắt bằng khối try/catch với khối try bọc ngoài đoạn mã sinh ngoại lệ và một khối catch phù hợp với loại ngoại lệ. Ta né bằng khai báo throws cho loại ngoại lệ đó ở đầu phương thức. Phương thức write của FileWriter có hai lựa chọn khi gọi new Printer(File): (1) bắt ngoại lệ như trong Hình 11.5. (2) né ngoại lệ để đẩy trách nhiệm cho nơi gọi nó như trong Hình 11.11. Trách nhiệm nay thuộc về main của WriteToFile.

Nếu một ngoại lệ ném ra sớm hay muộn cũng phải được bắt và xử lý, tại sao đôi khi ta nên trì hoãn việc đó? Lí do là không phải lúc nào ta cũng có đủ thông tin để có thể khắc phục sự cố một cách thích hợp. Giả sử ta là người viết lớp FileWriter cung cấp tiện ích xử lý file, và FileWriter được thiết kế để có thể dùng được cho nhiều ứng dụng khác nhau. Để xử lý sự cố ghi file – ngoại lệ FileNotFoundException, ta có thể làm gì tại phương thức write với chức năng như các ví dụ ở trên? Hiển thị lời thông báo lỗi? Yêu cầu cung cấp tên file khác? Im lặng không làm gì cả? Lẳng lặng ghi vào một file mặc định? Tất cả các giải pháp đó đều không ổn. Ta không thể biết hành động nào thì phù hợp với chính sách của ứng dụng đang chạy (nơi sử dụng FileWriter của ta), ta không có thẩm quyền để tự tương tác với người dùng (không rõ có hay không) hoặc tự thay đổi phương án với tên file khác. Đơn giản là, tại write, ta không có đủ thông tin để khắc phục sự cố. Vậy thì đừng làm gì cả, hãy tránh sang một bên để cho nơi có đủ thông tin xử lý nhận trách nhiệm.

Ngay cả khi lựa chọn bắt ngoại lệ để xử lý, một phương thức vẫn có thể ném tiếp chính ngoại lệ vừa bắt được sau khi đã xử lí một phần theo khả năng và trách nhiệm của mình. Ví dụ:


Hãy tham gia nhóm Học lập trình để thảo luận thêm về các vấn đề cùng quan tâm.

Leave a Reply

Your email address will not be published. Required fields are marked *