Backend

99 Ngày Java — Ngày 83: Sealed Classes

SSite Admin
18 tháng 10, 2026 3 phút đọc 4 lượt xem
99 Ngày Java — Ngày 83: Sealed Classes

Trước Java 17, một lớp hoặc là final, hoặc ai cũng kế thừa được. Sealed class thêm lựa chọn ở giữa: tác giả liệt kê chính xác những kiểu con được phép. Nhờ biết đủ danh sách, compiler kiểm tra được switch đã xử lý hết trường hợp hay chưa. Đây là cách Java biểu diễn một tập phương án đóng, ví dụ kết quả của một lần thanh toán.

Sealed Classes

1. sealed và permits

Từ khóa sealed đặt trên class hoặc interface; mệnh đề permits liệt kê kiểu con trực tiếp. Các kiểu con phải nằm cùng module với kiểu cha, hoặc cùng package nếu code không dùng module. Nếu tất cả kiểu con khai báo trong cùng file nguồn, có thể bỏ permits và compiler tự suy ra. Người ngoài vẫn dùng được kiểu cha, chỉ không thêm được phương án mới.

public sealed interface PaymentResult
        permits Approved, Declined, Pending {}

public record Approved(String transactionId) implements PaymentResult {}
public record Declined(String reason) implements PaymentResult {}
public record Pending(String checkUrl) implements PaymentResult {}

2. Ba lựa chọn của kiểu con

Mỗi kiểu con được phép phải tự nói nó đóng hay mở tiếp: final là dừng tại đây; sealed là tiếp tục giới hạn bằng danh sách riêng; non-sealed là mở lại cho mọi người kế thừa. Record và enum mặc nhiên final nên rất hợp làm lá của cây. non-sealed là lối thoát có chủ ý: nhánh đó không còn được kiểm soát, nhưng switch trên kiểu cha vẫn đầy đủ vì nó xử lý nhánh ấy như một trường hợp.

sealed interface Vehicle permits Car, Truck, Custom {}
final class Car implements Vehicle {}
sealed class Truck implements Vehicle permits Pickup {}
final class Pickup extends Truck {}
non-sealed class Custom implements Vehicle {} // open again

3. Switch đầy đủ, không cần default

Với kiểu sealed, switch có pattern cho mọi kiểu con được coi là đầy đủ, nên không cần default. Lợi ích thật nằm ở bảo trì: thêm Refunded vào permits thì mọi switch chưa xử lý nó sẽ không biên dịch. Nếu bạn viết default cho yên tâm, compiler mất khả năng chỉ ra chỗ thiếu, và phương án mới lặng lẽ rơi vào nhánh mặc định.

String message(PaymentResult result) {
    return switch (result) {
        case Approved a -> "Paid, ref " + a.transactionId();
        case Declined d -> "Declined: " + d.reason();
        case Pending p  -> "Waiting, check " + p.checkUrl();
    };
}

4. Mô hình hóa domain

Sealed interface cộng record tạo thành kiểu dữ liệu đại số: mỗi phương án mang đúng dữ liệu của nó, không có trường null dùng chung và không có cờ trạng thái phải nhớ kiểm tra. Hợp nhất khi tập phương án ổn định và thuộc quyền của bạn: trạng thái đơn hàng, kết quả lệnh, sự kiện domain, nút của cây cú pháp.

  • Dùng khi tập phương án đóng và bạn muốn compiler nhắc khi nó thay đổi.

  • Không dùng cho điểm mở rộng mà bên thứ ba cần tự thêm cài đặt; đó là việc của interface thường.

  • Khi serialize ra JSON, cấu hình thông tin kiểu rõ ràng và kiểm thử cả chiều đọc lại.

sealed biến câu "chỉ có ngần này trường hợp" từ lời chú thích thành điều compiler kiểm tra. Ngày 84 xem text block, cách viết chuỗi nhiều dòng gọn hơn.

Tài liệu đối chiếu

  • https://openjdk.org/jeps/409

  • https://docs.oracle.com/en/java/javase/21/language/sealed-classes-and-interfaces.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 83: R2DBC

Driver non-blocking, ReactiveCrudRepository, transaction reactive và những đánh đổi so với JPA.

18 thg 10, 20262 phút5
99 Ngày Spring — Ngày 82: Reactive Operators và Backpressure

map, flatMap, concatMap, zip, xử lý lỗi bằng onErrorResume và retryWhen, cùng khái niệm backpressure trong Reactor.

17 thg 10, 20263 phút6
99 Ngày Java — Ngày 82: Switch Expression và Pattern Matching

Cú pháp mũi tên, yield, type pattern, guard when, record pattern và tính đầy đủ của switch trên Java 21.

17 thg 10, 20263 phút7