Java OO & Functional
13/38
Bài 13 / 38~12 phútException HandlingLộ trình · chặng 04/21Miễn phí… lượt xem

Checked vs Unchecked — cuộc tranh luận lớn trong thiết kế Java

Phân biệt checked và unchecked exception, cơ chế compiler kiểm tra throws, xu hướng unchecked trong Java hiện đại và kỹ thuật uncheck cho Stream API.

TL;DR: Java là ngôn ngữ phổ biến duy nhất phân chia ngoại lệ thành hai nhóm: Checked (buộc phải khai báo throws hoặc bắt qua try-catch) và Unchecked (lớp con của RuntimeException, tự do truyền dẫn). Dù mục tiêu ban đầu là nâng cao độ tin cậy, Checked Exception lại tạo ra gánh nặng lớn về boilerplate, phá vỡ tính tương thích với Stream/Lambda, và thúc đẩy thói quen nuốt ngoại lệ. Hệ sinh thái Java hiện đại (Spring, Hibernate, Jackson) hầu hết đã chuyển dịch sang Unchecked Exception làm mặc định.

Ở bài trước, bạn đã thấy rằng tại tầng Bytecode, JVM dùng cùng một lệnh athrow và một cơ chế Exception Table chung cho mọi ngoại lệ. Khái niệm Checked Exception hoàn toàn là một lớp bảo vệ tĩnh do trình biên dịch javac thực thi trước khi mã được đóng gói.

Khi Java ra đời vào năm 1995, James Gosling và nhóm thiết kế đặt nhiều kỳ vọng vào Checked Exception: họ muốn các phương thức phải ký kết hợp đồng rõ ràng về mọi rủi ro có thể phát sinh. Tuy nhiên, sau gần 30 năm phát triển, không một ngôn ngữ hiện đại nào sau đó (như C#, Kotlin, Go, Rust, TypeScript, Swift) học tập mô hình Checked Exception của Java.

1. Định nghĩa kỹ thuật: Khác biệt ở tầng biên dịch javac

Bảng phân loại cốt lõi:

Tiêu chíChecked ExceptionUnchecked Exception
Cây kế thừaKế thừa trực tiếp từ Exception (trừ RuntimeException)Kế thừa từ RuntimeException hoặc Error
Trình biên dịch (javac)Bắt buộc phải viết try-catch hoặc khai báo throwsKhông kiểm tra, cho phép truyền tự do
Ví dụ tiêu biểuIOException, SQLException, ClassNotFoundExceptionNullPointerException, IllegalArgumentException
Ý định thiết kếTình huống ngoại vi bất khả kháng mà caller có thể xử lýLỗi logic mã nguồn hoặc sự cố hạ tầng không thể cứu vãn

Điểm mấu chốt: Khác biệt này chỉ tồn tại lúc biên dịch.

Hai cột thời điểm: javac thấy lời gọi có thể ném IOException, xác định IOException không kế thừa RuntimeException hay Error nên là checked, chưa khai throws chưa catch thì từ chối biên dịch; còn JVM dùng cùng lệnh athrow cho mọi Throwable và tra exception table theo cùng một quy trình

Trong cấu trúc file .class, máy ảo JVM không có bất kỳ cờ hiệu nào phân biệt hai loại này. Mọi ngoại lệ đều được kích hoạt bằng cùng một chỉ lệnh athrow và phân giải qua Exception Table.

2. Cách Checked Exception ép buộc lập trình viên

Khi một phương thức gọi hàm có khai báo checked exception (ví dụ đọc file danh mục thư viện):

Java
public String readCatalogJson(String path) {
    FileReader reader = new FileReader(path); // COMPILE ERROR: Unhandled exception type FileNotFoundException
    BufferedReader br = new BufferedReader(reader);
    return br.readLine();                     // COMPILE ERROR: Unhandled exception type IOException
}

Trình biên dịch buộc bạn phải chọn một trong hai con đường:

Lựa chọn 1: Đẩy trách nhiệm lên tầng trên bằng từ khoá throws

Java
public String readCatalogJson(String path) throws IOException {
    FileReader reader = new FileReader(path);
    BufferedReader br = new BufferedReader(reader);
    return br.readLine();
}

Nhược điểm: Bất kỳ ai gọi readCatalogJson (kể cả tầng Service hoặc Controller) đều bị lây nhiễm (signature pollution) và buộc phải tiếp tục khai báo throws IOException.

Lựa chọn 2: Xử lý tại chỗ bằng khối try-catch

Java
public String readCatalogJson(String path) {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        logger.error("Doc tap tin that bai: " + path, e);
        return null;
    }
}

3. Vì sao Checked Exception bị chỉ trích trong thực tế?

3.1 Ô nhiễm chữ ký hàm (Signature Pollution)

Hãy hình dung hệ thống quản lý mượn trả: Tầng Repository dùng JDBC nên ném SQLException. Tầng Service gọi Repository nên phải khai throws SQLException. Tầng Controller gọi Service cũng phải khai throws SQLException. Nếu sau này hệ thống chuyển từ PostgreSQL sang MongoDB (ném MongoException), bạn phải thay đổi chữ ký phương thức trên toàn bộ các tầng kiến trúc. Điều này phá vỡ hoàn toàn tính đóng gói (Encapsulation).

3.2 Xung đột với Stream API và Functional Programming

Kể từ Java 8, Functional Interfaces như Function<T, R> hay Consumer<T> không khai báo bất kỳ checked exception nào trong định nghĩa:

Java
List<String> filePaths = List.of("books.json", "members.json");

// LOI BIEN DICH: Unhandled exception: java.io.IOException
List<String> contents = filePaths.stream()
    .map(path -> readCatalogJson(path))
    .toList();

Biểu thức Lambda không thể ném checked exception nếu interface mục tiêu không cho phép. Điều này buộc lập trình viên phải chèn những khối try-catch vụng về vào ngay bên trong thân lambda.

Giải pháp chuẩn: Kỹ thuật uncheck Function Wrapper

Để giữ Stream API trong sáng, chúng ta xây dựng một Utility Helper:

Java
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Exception> {
    R apply(T t) throws E;

    static <T, R> Function<T, R> uncheck(ThrowingFunction<T, R, ?> function) {
        return arg -> {
            try {
                return function.apply(arg);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}

// Su dung cuc ky gon gang trong Stream:
List<String> contents = filePaths.stream()
    .map(ThrowingFunction.uncheck(this::readCatalogJson))
    .toList();

3.3 Khuyến khích thói quen xấu: Nuốt ngoại lệ

Khi bị compiler ép phải catch những ngoại lệ mà tại vị trí đó lập trình viên không biết phải làm gì để khôi phục (ví dụ InterruptedException khi đang chạy một tác vụ nền nhỏ), nhiều người có xu hướng viết khối catch rỗng. Kết quả là Checked Exception vô tình làm giảm độ an toàn của hệ thống thay vì tăng lên.

4. Nguyên tắc thiết kế từ Effective Java (Joshua Bloch)

Joshua Bloch đưa ra hai lời khuyên quan trọng trong Effective Java:

  • Item 70: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors (Dùng checked exception cho các tình huống có thể khôi phục được và dùng unchecked exception cho lỗi lập trình).
  • Item 71: Avoid unnecessary use of checked exceptions (Tránh lạm dụng checked exception).

Khi nào thực sự nên dùng Checked Exception?

Chỉ dùng khi thoả mãn đồng thời hai câu hỏi sau:

  1. Người gọi có thể làm gì để khôi phục không? (Ví dụ: Thẻ ngân hàng không đủ số dư -> người dùng có thể nạp thêm tiền hoặc chọn thẻ khác).
  2. Hành động khôi phục đó có thực tế không? Nếu câu trả lời duy nhất của tầng gọi là "ghi log và báo lỗi 500", đó chắc chắn phải là Unchecked Exception.

5. Quy tắc chuyển đổi: Exception Translation Pattern

Cách tiếp cận chuẩn mực của các framework hiện đại (như cách Spring Data chuyển đổi SQLException thành DataAccessException):

  • Bắt Checked Exception ở tầng giao tiếp hạ tầng ngoài cùng (I/O, Network, JDBC).
  • Bọc nó thành một Unchecked Domain Exception tương ứng và truyền ngoại lệ gốc vào cause.
Java
public class LibraryCatalogRepository {
    public void save(LibraryItem item) {
        try {
            databaseClient.insert(item);
        } catch (SQLException e) {
            // Chuyen doi checked SQLException thanh unchecked RepositoryException
            throw new LibraryStorageException("Loi luu tru an pham ID: " + item.getId(), e);
        }
    }
}

Caller ở tầng Service và Controller không còn bị ô nhiễm chữ ký hàm, nhưng toàn bộ vết tích kỹ thuật của SQLException vẫn được bảo toàn nguyên vẹn trong Call Stack log.

6. Deep Dive Oracle & Tài liệu tham khảo

Tài liệu tham khảo đặc tả
  • Effective Java (Joshua Bloch):
    • Item 70: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors.
    • Item 71: Avoid unnecessary use of checked exceptions.
    • Item 73: Throw exceptions appropriate to the abstraction (Áp dụng Exception Translation để bảo vệ trừu tượng hoá).
  • Java Generics and Collections (Maurice Naftalin & Philip Wadler): Thảo luận về xung đột cấu trúc giữa cơ chế Generics, Lambdas và Checked Exceptions.

7. Tự kiểm tra kiến thức

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Sự khác biệt cốt lõi giữa Checked Exception và Unchecked Exception nằm ở tầng nào trong kiến trúc Java?
  2. Q2
    Tại sao Checked Exception lại xung đột mạnh mẽ với Stream API từ Java 8 trở đi?
  3. Q3
    Tiêu chuẩn vàng theo Effective Java Item 70 để quyết định một ngoại lệ nên là Checked hay Unchecked là gì?
  4. Q4
    Pattern 'Exception Translation' giải quyết bài toán ô nhiễm chữ ký hàm (Signature Pollution) như thế nào?
  5. Q5
    Tại sao việc gượng ép dùng Checked Exception thường dẫn đến nguy cơ gia tăng các khối catch rỗng (nuốt ngoại lệ)?

Bài tiếp theo: try-with-resources — quản lý vòng đời tài nguyên tự động

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 4/21 · Exception handling — bài 4/7 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

try-with-resources — tự đóng resource không rò rỉ