Chuyển tới nội dung chính

Giới thiệu Exception Handling

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu exception là gì và tại sao cần xử lý exception trong Java
  • Phân biệt Error vs Exception và Checked vs Unchecked Exception
  • Nắm vững exception hierarchy từ Throwable đến các exception classes cụ thể
  • Nhận biết các exception phổ biến: NullPointerException, ArrayIndexOutOfBoundsException, IOException
  • Hiểu hậu quả khi không handle exception và lợi ích khi handle đúng cách

Exception là gì?

Exception (ngoại lệ) là một sự kiện bất thường xảy ra trong quá trình thực thi chương trình, làm gián đoạn luồng thực thi bình thường của code. Khi một exception xảy ra, chương trình sẽ crash (ngừng hoạt động) nếu không được xử lý đúng cách.

Tại sao cần xử lý Exception?
  • Tránh crash chương trình: Giúp ứng dụng vẫn hoạt động ngay cả khi có lỗi
  • Tạo trải nghiệm người dùng tốt hơn: Hiển thị thông báo lỗi dễ hiểu thay vì crash
  • Debug dễ dàng hơn: Biết chính xác lỗi xảy ra ở đâu và nguyên nhân gì
  • Dọn dẹp tài nguyên: Đảm bảo đóng file, connection, etc. ngay cả khi có lỗi
  • Tách biệt logic xử lý lỗi: Code sạch hơn, dễ maintain hơn

Exception Hierarchy

Java có một hệ thống phân cấp exception rõ ràng. Hãy tưởng tượng như phân loại sự cố giao thông:

  • Throwable = "Sự cố giao thông" (tất cả các sự cố trên đường)
    • Error = "Thiên tai / sập cầu" — bạn không xử lý được, chỉ có thể tránh đi đường đó
    • Exception = "Sự cố do xe / người lái" — bạn có thể xử lý hoặc phòng tránh
      • Checked Exception = "Hết xăng, nổ lốp" — bạn buộc phải mang theo xăng dự phòng / lốp sơ cua
      • Unchecked Exception = "Vượt đèn đỏ, quên xi-nhan" — lỗi do bạn, sửa cách lái là hết
                    Object
|
Throwable
/ \
Error Exception
| |
(system-level) |
/ \
/ \
IOException RuntimeException
SQLException |
etc. |
|
NullPointerException
ArrayIndexOutOfBoundsException
ArithmeticException
ClassCastException
etc.

Chi tiết Hierarchy

  1. Throwable: Root class của tất cả exception và error

    • Có 2 subclass chính: ErrorException
  2. Error: Lỗi nghiêm trọng ở system-level, không nên catch

    • OutOfMemoryError: Hết bộ nhớ
    • StackOverflowError: Stack bị tràn (thường do recursion vô hạn)
    • VirtualMachineError: Lỗi JVM
  3. Exception: Lỗi có thể xử lý được

    • Checked Exception: Compiler bắt buộc phải xử lý
    • Unchecked Exception (RuntimeException): Không bắt buộc xử lý

Checked vs Unchecked Exceptions

Đặc điểmChecked ExceptionUnchecked Exception
Parent classException (không phải RuntimeException)RuntimeException
Bắt buộc xử lýCó (compile-time)Không
Khi nào xảy raLỗi bên ngoài tầm kiểm soát (I/O, network, database)Lỗi lập trình (logic bug, null reference)
Ví dụIOException, SQLException, ClassNotFoundExceptionNullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException
Xử lýBắt buộc dùng try-catch hoặc throwsTùy chọn xử lý
PreventionKhông thể prevent hoàn toànCó thể prevent bằng code tốt hơn
Checked Exception

Nếu method có thể throw checked exception mà không xử lý, phải khai báo trong method signature với throws:

public void readFile() throws IOException {
// code đọc file
}

Common Exceptions trong Java

1. NullPointerException (Unchecked)

Exception phổ biến nhất! Xảy ra khi truy cập method/field của object null.

String text = null;
System.out.println(text.length()); // ❌ NullPointerException

// Cách tránh:
if (text != null) {
System.out.println(text.length()); // ✅ Safe
}

// Hoặc dùng Optional (Java 8+)
Optional<String> optionalText = Optional.ofNullable(text);
optionalText.ifPresent(t -> System.out.println(t.length()));

2. ArrayIndexOutOfBoundsException (Unchecked)

Xảy ra khi truy cập index không tồn tại trong array.

int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ❌ ArrayIndexOutOfBoundsException

// Cách tránh:
if (index >= 0 && index < numbers.length) {
System.out.println(numbers[index]); // ✅ Safe
}

3. ClassCastException (Unchecked)

Xảy ra khi cast object sang type không tương thích.

Object obj = "Hello";
Integer num = (Integer) obj; // ❌ ClassCastException

// Cách tránh:
if (obj instanceof Integer) {
Integer num = (Integer) obj; // ✅ Safe
}

4. ArithmeticException (Unchecked)

Xảy ra khi có phép toán không hợp lệ, ví dụ chia cho 0.

int result = 10 / 0; // ❌ ArithmeticException

// Cách tránh:
int divisor = 0;
if (divisor != 0) {
int result = 10 / divisor;
} else {
System.out.println("Cannot divide by zero");
}
Lưu ý với floating point
double result = 10.0 / 0.0; // ✅ Không throw exception
System.out.println(result); // In ra: Infinity

Floating point division by zero không throw exception, trả về Infinity hoặc NaN.

5. IOException (Checked)

Xảy ra khi có lỗi I/O (đọc/ghi file, network, etc.).

// ❌ Code này không compile vì không xử lý IOException
public void readFile() {
FileReader reader = new FileReader("data.txt");
}

// ✅ Phải xử lý checked exception
public void readFile() throws IOException {
FileReader reader = new FileReader("data.txt");
}

// Hoặc dùng try-catch
public void readFile() {
try {
FileReader reader = new FileReader("data.txt");
} catch (IOException e) {
System.out.println("File not found: " + e.getMessage());
}
}

6. FileNotFoundException (Checked)

Subclass của IOException, xảy ra khi file không tồn tại.

try {
FileInputStream fis = new FileInputStream("nonexistent.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e.getMessage());
}

Error vs Exception

Error

Error là lỗi nghiêm trọng ở system-level mà application không nên cố gắng catch. Đây là lỗi JVM hoặc hardware.

// ❌ KHÔNG NÊN catch Error
try {
// some code
} catch (Error e) {
// Bad practice!
}

Ví dụ Error phổ biến:

1. OutOfMemoryError

Xảy ra khi JVM hết heap memory.

// Code này sẽ throw OutOfMemoryError
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // Thêm 1MB mỗi lần
}

2. StackOverflowError

Xảy ra khi stack bị tràn, thường do recursion không có điều kiện dừng.

// ❌ Recursion vô hạn
public void infiniteRecursion() {
infiniteRecursion(); // Gọi chính nó mãi mãi
}
Khi nào xảy ra Error?
  • Hết memory (OutOfMemoryError)
  • Stack overflow (StackOverflowError)
  • Lỗi JVM (VirtualMachineError)
  • Lỗi link class (LinkageError)

Giải pháp: Fix root cause (tăng memory, fix logic), KHÔNG catch Error!

Exception

Exception là lỗi có thể xử lý được trong application code. Nên catch và xử lý phù hợp.

Ví dụ thực tế: Chương trình crash khi không handle exception

Ví dụ 1: Không handle exception

public class WithoutExceptionHandling {
public static void main(String[] args) {
System.out.println("Program started");

// ❌ Không xử lý exception
String text = null;
System.out.println(text.length()); // NullPointerException!

// Code sau đây sẽ KHÔNG BAO GIỜ chạy
System.out.println("Program ended"); // ❌ Never executed
}
}

Output:

Program started
Exception in thread "main" java.lang.NullPointerException
at WithoutExceptionHandling.main(WithoutExceptionHandling.java:6)

Chương trình crash tại dòng lỗi, code phía sau không chạy.

Ví dụ 2: Có handle exception

public class WithExceptionHandling {
public static void main(String[] args) {
System.out.println("Program started");

try {
// ✅ Xử lý exception trong try-catch
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
System.out.println("Error: text is null!");
}

// Code này VẪN chạy bình thường
System.out.println("Program ended"); // ✅ Executed!
}
}

Output:

Program started
Error: text is null!
Program ended

Chương trình không crash, xử lý lỗi gracefully và tiếp tục chạy.

Ví dụ 3: Real-world scenario - Banking System

public class BankingSystem {
public static void main(String[] args) {
System.out.println("Banking System Started");

try {
// Giả lập rút tiền
withdraw(1000, 500); // ✅ OK: Rút 500 từ tài khoản có 1000
withdraw(200, 500); // ❌ ERROR: Số dư không đủ

} catch (ArithmeticException e) {
System.out.println("Transaction failed: " + e.getMessage());
// Log error, gửi notification, etc.
}

System.out.println("Banking System Still Running"); // ✅ System vẫn chạy
}

public static void withdraw(int balance, int amount) {
if (amount > balance) {
throw new ArithmeticException("Insufficient balance");
}
System.out.println("Withdrawal successful: " + amount);
}
}

Output:

Banking System Started
Withdrawal successful: 500
Transaction failed: Insufficient balance
Banking System Still Running
Key Takeaway
  • Không xử lý exception → Chương trình crash, mất dữ liệu, trải nghiệm xấu
  • Có xử lý exception → Chương trình ổn định, xử lý lỗi gracefully, log được lỗi
  • Exception handling là bắt buộc cho production code!

Tại sao Java có Checked Exceptions?

Nhiều ngôn ngữ hiện đại (C#, Kotlin, Python) không có checked exceptions — tất cả đều là unchecked. Vậy tại sao Java lại thiết kế khác?

Triết lý thiết kế

Java ra đời năm 1995 với triết lý: "Buộc developer phải nghĩ về lỗi ngay khi viết code". Checked exceptions là cách compiler nhắc bạn: "Hé, operation này có thể fail — bạn định làm gì nếu nó fail?"

// Compiler nhắc: "File có thể không tồn tại, bạn xử lý chưa?"
public void readConfig() throws IOException {
FileReader reader = new FileReader("config.txt");
// ...
}

Ưu và nhược điểm

Checked ExceptionUnchecked Exception
Ưu điểmCompiler nhắc xử lý lỗi, API rõ ràng hơnCode gọn hơn, linh hoạt hơn
Nhược điểmVerbose, "throws pollution" lan khắp call chainDễ quên xử lý, runtime mới biết

Khi nào dùng loại nào?

  • Checked: Khi caller có thể và nên recover — I/O errors, network timeouts, business rule violations
  • Unchecked: Khi lỗi là bug lập trình cần fix code — null reference, invalid argument, array out of bounds
Xu hướng hiện đại

Spring Framework, Hibernate, và nhiều thư viện Java hiện đại prefer unchecked exceptions. Spring wrap hầu hết checked exceptions (như SQLException) thành unchecked (DataAccessException). Lý do: để developer chọn xử lý thay vì bị buộc xử lý.

Exception Propagation — Lan truyền Exception

Khi một exception xảy ra, nó "bay lên" qua call stack cho đến khi gặp catch block phù hợp. Nếu không có catch nào bắt được, chương trình crash.

  main()                    ← 4. Nếu không ai bắt → CRASH!


processOrder() ← 3. Không catch → tiếp tục bay lên


validatePayment() ← 2. Không catch → bay lên caller


chargeCard() ← 1. Exception xảy ra tại đây!
💥 PaymentException
public static void main(String[] args) {
try {
processOrder(); // Bắt exception ở đây
} catch (Exception e) {
System.out.println("Order failed: " + e.getMessage());
}
}

static void processOrder() {
validatePayment(); // Không catch, exception bay lên main()
}

static void validatePayment() {
chargeCard(); // Không catch, exception bay lên processOrder()
}

static void chargeCard() {
throw new RuntimeException("Card declined"); // Exception bắt đầu từ đây
}
Quy tắc propagation
  • Unchecked exception: Tự động propagate, không cần khai báo throws
  • Checked exception: Phải khai báo throws hoặc catch — nếu không, compile error

Exception trong Method Overriding

Khi override method ở subclass, có quy tắc nghiêm ngặt về exception:

OCP Trap — Overriding Exception Rules

Method con KHÔNG THỂ ném broader checked exception hơn method cha.

Đây là hệ quả của Liskov Substitution Principle (LSP): nếu bạn dùng biến kiểu Parent nhưng object thực tế là Child, code xử lý exception của Parent phải vẫn đủ để bắt hết exception từ Child.

6 Scenarios — OK và FAIL

class Parent {
void doWork() throws IOException { }
}
#Child overrideKết quảLý do
1void doWork() throws IOException✅ OKCùng exception
2void doWork() throws FileNotFoundException✅ OKNarrower (FileNotFoundException extends IOException)
3void doWork()✅ OKKhông throw gì cũng được
4void doWork() throws Exception❌ FAILBroader hơn IOException
5void doWork() throws IOException, SQLException❌ FAILThêm checked exception mới
6void doWork() throws IOException, ArithmeticException✅ OKUnchecked exception không bị giới hạn
class Child extends Parent {
// ✅ OK: narrower exception
@Override
void doWork() throws FileNotFoundException {
throw new FileNotFoundException("not found");
}
}

class BadChild extends Parent {
// ❌ COMPILE ERROR: Exception is broader than IOException
@Override
void doWork() throws Exception { // Compile error!
throw new Exception("oops");
}
}
Ghi nhớ
  • Checked exception: Method con chỉ được throw same hoặc narrower (hoặc không throw gì)
  • Unchecked exception: Thoải mái — không bị giới hạn bởi rule này
  • Rule này chỉ áp dụng khi override (cùng signature), không áp dụng khi overload

Tóm tắt

Khái niệmMô tả
ExceptionSự kiện bất thường làm gián đoạn luồng thực thi
ErrorLỗi nghiêm trọng ở system-level, không nên catch
Checked ExceptionBắt buộc xử lý tại compile-time (IOException, SQLException)
Unchecked ExceptionKhông bắt buộc xử lý (RuntimeException, NullPointerException)
try-catchCơ chế xử lý exception (sẽ học ở bài tiếp theo)
Exception PropagationException "bay lên" call stack cho đến khi được catch
Overriding RuleMethod con không thể throw broader checked exception

Bài tập

  1. Bài 1: Viết chương trình chia 2 số nhập từ bàn phím. Chương trình crash khi nhập số chia = 0. Quan sát và ghi lại exception nào được throw.

  2. Bài 2: Tạo một array có 5 phần tử. Viết code cố gắng truy cập phần tử thứ 10. Quan sát exception.

  3. Bài 3: Giải thích tại sao code sau compile error:

public void readFile() {
FileReader reader = new FileReader("data.txt");
}
  1. Bài 4: Phân biệt:

    • NullPointerException vs FileNotFoundException
    • Error vs Exception
    • Checked vs Unchecked exception
  2. Bài 5: Viết method parseInt(String str) convert String sang int. Xử lý trường hợp String không phải là số hợp lệ (sử dụng Integer.parseInt() - nó throw NumberFormatException).

Bài tiếp theo

Trong bài tiếp theo, chúng ta sẽ học cách xử lý exception với try-catch-finallytry-with-resources!

Đọc thêm