Menu

Exception trong Java: lỗi, stack trace và throw

Ngoại lệ (exception) trong Java là gì, cách đọc stack trace, khác biệt giữa checked và unchecked, cây phân cấp ngoại lệ và cách tự ném ngoại lệ.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Ngoại lệ là gì

Ngoại lệ (exception) là cách Java nói "có gì đó sai rồi và tôi không thể chạy tiếp như bình thường." Thay vì trả về một giá trị sai hoặc lặng lẽ làm hỏng dữ liệu, môi trường chạy tạo ra một đối tượng ngoại lệ mô tả vấn đề và ném (throw) nó đi. Việc thực thi bình thường dừng ngay tại đó, và Java bắt đầu tìm đoạn code biết cách xử lý tình huống này.

Nếu không ai xử lý, ngoại lệ lan lên tận đỉnh chương trình, JVM in ra stack trace, và tiến trình thoát với mã trạng thái khác 0.

Để ý rằng "after" không bao giờ được in ra. Ngay khi numbers[5] được tính, một ArrayIndexOutOfBoundsException bị ném và phần còn lại của main bị bỏ qua. Truy cập vượt quá chỉ số hợp lệ cuối cùng của một mảng là một trong những cách phổ biến nhất gây ra ngoại lệ.

Đọc stack trace

Khi một ngoại lệ không được xử lý, bạn sẽ thấy đầu ra như thế này. Trông có vẻ đáng sợ, nhưng nó chỉ là một danh sách:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.divide(Main.java:8)
	at Main.main(Main.java:4)

Đọc từ trên xuống dưới:

  • Dòng đầu tiên là loại ngoại lệ (ArithmeticException) và thông báo (/ by zero).
  • Mỗi dòng at ... là một stack frame. Dòng trên cùng là nơi ngoại lệ thực sự bị ném: divide ở dòng 8. Bên dưới là hàm gọi nó, main ở dòng 4.

Dòng at trên cùng là chỗ cần xem trước tiên. Các frame bên dưới trả lời câu hỏi "làm sao chương trình đi tới đây?"

Chạy đoạn code này và stack trace sẽ chỉ thẳng vào dòng a / b, rồi cho thấy main là hàm gọi. Chuỗi lời gọi đó là công cụ gỡ lỗi hữu ích nhất mà Java cho bạn miễn phí.

Cây phân cấp ngoại lệ

Mọi ngoại lệ đều là đối tượng, và tất cả đều kế thừa từ Throwable. Hai nhánh quan trọng:

  • Error: những vấn đề nghiêm trọng do JVM phát sinh, như OutOfMemoryError hay StackOverflowError. Nói chung bạn không bắt những lỗi này.
  • Exception: những vấn đề mà chương trình có thể lường trước và xử lý. Bên trong nó là RuntimeException, lớp cha của các bug thường ngày như NullPointerException.
Throwable
├── Error                 (don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
    ├── IOException        (checked)
    ├── SQLException       (checked)
    └── RuntimeException   (unchecked)
        ├── NullPointerException
        ├── ArithmeticException
        └── ArrayIndexOutOfBoundsException

Vì đây là các lớp thật, một ngoại lệ có thể mang theo thông báo và bạn có thể hỏi thông tin về chính nó:

getMessage() trả về phần văn bản sau dấu hai chấm trong stack trace; getClass().getSimpleName() cho bạn tên của loại ngoại lệ.

Checked và unchecked

Đây là điểm khiến người mới học hay vấp nhất.

  • Ngoại lệ unchecked kế thừa RuntimeException. Chúng thường báo hiệu bug trong code: một null không ngờ tới, một chỉ số sai, chia cho 0. Trình biên dịch không bắt buộc bạn xử lý chúng.
  • Ngoại lệ checked kế thừa Exception nhưng không kế thừa RuntimeException (ví dụ IOException). Chúng đại diện cho những tình huống nằm ngoài tầm kiểm soát: thiếu tệp, mất kết nối mạng. Trình biên dịch buộc bạn phải bắt hoặc khai báo chúng.

Nếu một phương thức có thể ném checked exception, nó phải khai báo bằng throws, và mọi nơi gọi nó đều phải xử lý:

Bỏ throws Exception khỏi main thì code thậm chí không biên dịch được: đó là trình biên dịch đang thực thi quy ước của checked exception. Unchecked exception không bao giờ đòi hỏi điều này.

Tự ném ngoại lệ

Bạn không chỉ phản ứng với ngoại lệ mà còn có thể tự tạo ra chúng. Dùng throw với một đối tượng ngoại lệ mới để báo rằng một đối số hoặc trạng thái không hợp lệ. Cách này tốt hơn nhiều so với trả về một giá trị đặc biệt như -1 rồi hy vọng nơi gọi sẽ kiểm tra.

Luôn kèm theo thông báo giải thích điều gì sai, và tốt nhất là cả giá trị gây lỗi: chính bạn sau này khi đọc stack trace sẽ thấy biết ơn. IllegalArgumentException và IllegalStateException là hai ngoại lệ bạn sẽ dùng nhiều nhất khi kiểm tra dữ liệu đầu vào.

Lỗi thường gặp

  • Nuốt ngoại lệ. Bắt ngoại lệ rồi không làm gì (một khối rỗng) sẽ che giấu bug. Ít nhất hãy ghi log; thường thì bạn nên xử lý hoặc ném lại nó.
  • Bắt Exception quá rộng. Bắt lớp gốc Exception (tệ hơn nữa là Throwable) có thể che mất những vấn đề bạn không định xử lý. Hãy bắt đúng loại ngoại lệ bạn mong đợi.
  • Đọc stack trace từ dưới lên. Frame quan trọng nhất là dòng at trên cùng, không phải dưới cùng. Hãy bắt đầu từ đó.
  • Nhầm Error với Exception. Đừng cố khôi phục sau OutOfMemoryError hay StackOverflowError; hãy sửa nguyên nhân gốc.

Tiếp theo: try-catch

Giờ bạn đã biết ngoại lệ là gì và cách đọc chúng. Trang tiếp theo hướng dẫn cách thực sự xử lý chúng: bọc đoạn code rủi ro trong khối try, khôi phục trong catch, và chạy code dọn dẹp trong finally để chương trình tiếp tục chạy thay vì dừng đột ngột.

Câu hỏi thường gặp

Exception trong Java là gì?

Ngoại lệ (exception) là một đối tượng đại diện cho một vấn đề được phát hiện khi chương trình đang chạy, như chia cho 0, truy cập vượt quá cuối mảng, hoặc gọi phương thức trên null. Khi vấn đề xảy ra, Java ném (throw) ngoại lệ: nó dừng luồng chạy bình thường và tìm đoạn code có thể xử lý ngoại lệ đó. Nếu không có gì xử lý, chương trình in ra stack trace và thoát.

Checked exception và unchecked exception trong Java khác nhau thế nào?

Checked exception (lớp con của Exception nhưng không phải RuntimeException, ví dụ IOException) bắt buộc phải được bắt hoặc khai báo bằng throws, và trình biên dịch kiểm tra điều đó. Unchecked exception (lớp con của RuntimeException, ví dụ NullPointerException, ArrayIndexOutOfBoundsException) thường báo hiệu bug trong code và không cần khai báo. Error (như OutOfMemoryError) cũng là unchecked và nói chung không nên bắt.

Làm sao đọc stack trace trong Java?

Đọc từ trên xuống. Dòng đầu tiên cho biết loại ngoại lệ và thông báo (ví dụ java.lang.ArithmeticException: / by zero). Mỗi dòng at ... bên dưới là một stack frame cho biết phương thức, tệp và số dòng, bắt đầu từ nơi ngoại lệ bị ném rồi lần ngược qua các hàm gọi. Dòng at trên cùng gần như luôn là chỗ cần xem trước tiên.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu