[Java] Xử lý ngoại lệ trong Java – Exceptions Handling

Exception

Một exception (ngoại lệ) là một vấn đề phát sinh trong quá trình thực hiện chương trình. Một exception có thể xảy ra vì nhiều lý do khác nhau, ví dụ:

– Một người dùng đã nhập dữ liệu không hợp lệ.

– Một tập tin cần được mở ra nhưng không tìm thấy tập tin đó.

– Một kết nối mạng bị mất giữa chừng v.v…

Những ngoại lệ này xảy ra có thể do lỗi người dùng, hoặc có thể do lỗi của các lập trình viên, hoặc cũng có thể do tài nguyên vật lý trên hệ thống không đáp ứng được nhu cầu…

Khi một exception như vậy xảy ra nếu không được xử lý thì sẽ làm cho chương trình ngừng hoạt động, không bền vững, tốn tài nguyên, không cho kết quả thực thi như mong muốn. Vì vậy trong Java đã cung cấp cho chúng ta một cơ chế dùng để bẫy lỗi gọi là Exception Handling (Cơ chế xử lý ngoại lệ). Việc xử lý này làm hạn chế tối đa trường hợp hệ thống bị crash, ngắt đột ngột. Tính năng này làm cho Java là một ngôn ngữ lập trình mạnh 🙂

Để hiểu cơ chế xử lý ngoại lệ làm việc như thế nào trong Java thì trước tiên chúng ta cần hiểu 3 khái niệm: Checked Exception, Unchecked Exception, Error.

Hệ thống phân cấp các lớp exceptions:

Exception Hierarchy

Quan sát hệ thống phân cấp thì ta có thể thấy cha của tất cả các Errors và Exceptions trong ngôn ngữ Java là lớp Throwable (lớp này thừa kế từ lớp Object)

1. Error:

Đây là vấn đề phát sinh ngoài sự kiểm soát của người sử dụng hoặc các lập trình viên. Ví dụ một ngăn xếp bị tràn thì một Error sẽ phát sinh hoặc ứng dụng mở một tập tin thành công nhưng khi đọc dữ liệu lại không đọc được do ổ cứng bị hỏng một vài sector vật lý v.v… Javadoc đã giải thích rõ rằng:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.

Tức là hầu hết các Errors là trường hợp bất thường xảy ra, các lập trình viên không phải làm bất cứ điều gì với Error cả. Và đó là một ý tưởng tồi nếu bạn dùng try – catch cho Errors. Thông thường, việc chương trình phục hồi lại từ một Error là không thể và chương trình nên kết thúc. Ví dụ: OutOfMemoryError, StackOverflowError, v.v…

2. Checked Exceptions:

Là những exceptions xảy ra trong lúc biên dịch. Class Exception và các lớp con của nó đều là Checked Exceptions (ngoại trừ class RuntimeException – các lớp con của RuntimeException & Class Error – các lớp con của Error).

3. Unchecked Exceptions:

Thường được hiểu là những exceptions được sinh ra do lỗi logic khi lập trình và được trả về lúc runtime. Lớp RuntimeException và tất cả các lớp con của lớp RuntimeException đều là Unchecked Exception.

Tóm lại: Điểm khác biệt giữa các lớp checked và unchecked expcetion chính là thời điểm xác định được expcetion có thể xảy ra. Đối với checked exception, việc kiểm tra được thực hiện ngay thời điểm compile time, một số IDE (Netbeans, Eclipse) sẽ giúp chúng ta bằng cách hiển thị lỗi cú pháp nếu ta gọi một method throw ra bất kỳ checked exception nào mà không được catch. Một số checked exception tiêu biểu như: IOException, InterruptedException, XMLParseException…Còn đối với unchecked exception, việc xác định có exception xảy ra hay không chỉ có thể thực hiện ở thời điểm runtime, và các IDE sẽ không giúp chúng ta xác định được chuyện đó. Một số unchecked exception tiêu biểu là: NullPointerException, IndexOutOfBoundsException, ClassCastException…

Có một số điểm cần lưu ý ở đây:

– RuntimeException và các lớp con của của nó là một tập hợp con của Unchecked Exceptions. Và vì vậy RuntimeException và Unchecked Exceptions không phải là hai từ đồng nghĩa như nhiều người vẫn lầm tưởng.

– Lớp Error và các lớp con của nó cũng không được kiểm tra tại thời điểm biên dịch nên cũng được coi là Unchecked Exceptions.

Để hiểu hơn thì chúng ta xem hình ảnh minh họa sau:

Checked Exceptions và Unchecked Expcetions

Một số ví dụ về Checked Exceptions & Unchecked Exceptions:

Ex1:

exception_1

Với ví dụ trên:

– Khi chuyển chuỗi sang số nguyên trình biên dịch không hề báo lỗi mặc dù chuỗi “ABC” không thể chuyển sang số nguyên – Unchecked Exceptions

– Khi chuyển chuỗi sang thời gian trình biên dịch thông báo lỗi mặc dù chuỗi “27/07/2014” hoàn toàn phù hợp với định dạng khiến chương trình không dịch được – Checked Exceptions

Ex2:

exception_2

Đoạn code này sẽ không compile được vì method go() khai báo là throws Exception (checked exceptions) trong khi method run() gọi đến method go() mà không dùng try/catch block và cũng không khai báo throws Exception

Còn đoạn code sau thì sao:

exception_3

Đoạn code này compile bình thường vì java.lang.ArrayIndexOutOfBoundsException là unchecked exceptions.

Một số phương thức quan trọng trong lớp Throwable

  • getMessage(): Trả về kiểu chuỗi lấy ra thông tin của lỗi, dùng trong khối catch(). Nếu getMessage() trả về null tức là ngoại lệ đó không có thông tin được lưu dưới dạng chuỗi trong đó.
  • toString(): Trả về chuỗi ghi tên của loại exception cùng với kết quả của getMessage()
  • printStackTrace(): In ra kết quả của toString() hiển thị thông điệp lỗi và chi tiết lỗi trên dòng nào, dùng trong khối catch, trả về kiểu void.

exception_4

Với getMessage()

exception_5

Với toString()

exception_5_1

Với printStackTrace()

exception_5_2

Các từ khóa hỗ trợ xử lý Exception: Try, Catch, Throw, Throws, Finally

Minh họa sự thực hiện của các khối ‘try’, ‘catch’ và ‘finally’

a. Try & Catch

Để bắt một exception ta dùng sự kết hợp của hai từ khóa try & catch

Cú pháp:

exception_syntax_1

Ex3:

exception_6_1

Kết quả:

exception_6

b. Multiple catch Blocks

Một khối try có thể đi kèm với nhiều khối catch

Cú pháp:

exception_syntax_2

Ex4:

exception_7

Kết quả:

exception_7_1

c. The finally Keyword

Dùng từ khóa finally tạo ra một khối code theo sau khối try. Khối code đó luôn luôn được thực thi bất kể có xảy ra exceptions hay không

Ex5:

exception_8

Kết quả:

exception_8_1

Chú ý:

  • “Try” có thể không có “Catch” nhưng không đứng một mình nhất định phải có “Finally”
  • “Catch” nhất định phải có “Try”
  • Đối với một “Try” có thể có nhiều “Catch” nhưng không được “Catch ngược”

Ta có thể dùng nhiều catch để bắt chính xác loại ngoại lệ sẽ gặp phải, tuy nhiên, nhiều khi ta không thể lường hết được, biết đâu có 1 ngoại lệ nào đó (chẳng hạn RuntimeException) nó ko vào một cái catch nào (ví dụ bạn có 3 catch: NumberFormatException, NullPointerException, FileNotFoundException) thì lúc đó chương trình của bạn vẫn bị crash. Để tránh tình trạng này, ta nên cẩn thận (mặc dù có thể như thế là thừa thãi nhưng đôi lúc lại có giá trị) đó là luôn để vòng catch cuối cùng là lớp Exception, như vậy nếu các vòng catch cụ thể ko gặp thì thằng catch này sẽ bắt được, và như thế sẽ ko bị crash. Và nếu mình không cần quan tâm quá nhiều đến ngoại lệ thì tốt nhất chỉ cần 1 catch(Exception e) là đủ.

Giải thích chỗ: Đối với một “Try” có thể có nhiều “Catch” nhưng không được “Catch” ngược

Lấy ví dụ với đoạn code Ex4, nếu ta viết như vậy thì không có vấn đề gì, nhưng nếu ta đổi thằng catch thứ 3 lên đầu thì sẽ báo lỗi ngay lập tức:

exception_9

Vì: thằng catch thứ 3 là cha của tất cả các exception, ta khai báo về đầu thì tất cả những thằng sau chả bao giờ được sử dụng nữa nên nó sẽ báo lỗi (vì có ngoại lệ thì nó sẽ nhảy luôn vào thằng đầu tiên)

  • Nguyên tắc khai báo catch là thằng ở trên phải là con của thằng dưới

Còn nếu như chỉ có 2 thằng catch đầu tiên thì vì nó không có quan hệ cha con nên ta đặt thằng nào trước cũng được.

d. The throws/throw Keywords

  • Throw: để quăng ra Exception ở bất kỳ dòng nào trong phương thức (sau đó dùng try-catch để bắt hoặc throws cho thằng khác sử lý)
  • Throws: Chỉ có phương thức mới được sử dụng. Khi một phương thức có throw bên trong mà không bắt lại (try – catch) thì phải ném đi (throws) cho thằng khác xử lý.

Chú ý: Nếu phương thức đang ném ra lỗi (throws), mà phương thức khác gọi phương thức đang ném ra lỗi thì phương thức đó vẫn phải ném ra lỗi hoặc xử lý luôn (try – catch)

Vậy khi nào dùng “Try-Catch”, khi nào dùng “Throws”?

Khối “Try-Catch” dùng để xử lý luôn lỗi, còn “Throws” dùng để ném lỗi đi cho thằng khác xử lý, và dùng “Throws” khi ta không muốn đặt nhiều khối “Try-Catch” bên trong.

exception_10

Sau này chỗ nào dùng MyMethod() đều phải bắt các Exception1, Exception2 lại. Exception3 đã đc catch trong MyMethod() nên không cần bắt lại nữa.

exception_11

Ex6: Sử dụng Throw và Throws

exception_12

Kết quả: Khi nhập mẫu bằng 0

exception_12_1

Kết quả: Khi nhập mẫu khác 0

exception_12_2

Chú ý: Nếu không muốn phương thức ps() throws ArithmeticException thì ta đặt khối lệnh if-else có chứa throw new ArithmeticException() trong khối “Try-Catch”. Và phương thức ps() trong hàm main() không cần try-catch nữa.

exception_12_3

4. Ngoại lệ tự định nghĩa

Ngoài việc dùng lớp đối tượng có sẵn trong Java. Ta có thể tự tạo cho mình các lớp ngoại lệ bằng cách kế thừa Exception hoặc lớp dẫn xuất của nó … Sau đó thì cài đặt:

Ex7:

exception_13

Kết quả:

exception_13_1

Ex8: Ví dụ đơn giản trong rút tiền ngân hàng, khi người dùng rút tiền mà vượt quá số tiền có trong tài khoản, thì sẽ không có Exception hệ thống nào giúp mình làm điều đó, vì thế chúng ta phải định nghĩa Exception khác phù hợp với nhu cầu thực tế.

B1: Ta tạo ra class TaiKhoanNganHang để thực hiện các tác vụ gửi tiền, rút tiền và một class BankingException để xử lý nếu có ngoại lệ xảy ra khi ta rút số tiền vượt quá trong tài khoản.

Class TaiKhoanNganHang

exception_14

Class BankingException

exception_14_1

B2: Tạo class RutTienNganHang

exception_14_2

Kết quả: Khi nhập số tiền lớn hơn số tiền trong tài khoản

exception_14_3

Kết quả: Khi nhập số tiền nhỏ hơn số tiền trong tài khoản

exception_14_4

Hết!!! 😉

Advertisement