Java OO & Functional
7/38
Bài 7 / 38~12 phútKế thừa & Đa hìnhLộ trình · chặng 03/21Miễn phí… lượt xem

Sealed class — kiểm soát tập subtype với pattern matching exhaustive

Sealed class/interface trong Java 17+, từ khóa permits, quy tắc final/sealed/non-sealed, và kỹ thuật kết hợp Record với Pattern Matching Switch.

TL;DR: Sealed class và interface (Java 17+) cho phép kiểm soát chính xác những lớp nào được phép kế thừa thông qua mệnh đề permits. Khi kết hợp với Record và Switch Pattern Matching (Java 21), lập trình viên có thể xây dựng cấu trúc Algebraic Data Types (ADT) an toàn tuyệt đối tại thời điểm biên dịch, loại bỏ nguy cơ sót trường hợp xử lý mà không cần dùng đến nhánh default.

Ở bài trước, chúng ta đã thấy Interface là một hợp đồng năng lực rất mạnh mẽ vì nó cho phép đa hiện thực. Tuy nhiên, tính chất mở mặc định của interface cũng mang lại một nhược điểm lớn: bất kỳ ai trong dự án hoặc thư viện bên ngoài cũng có thể tự ý implements interface đó. Khi bạn cần mô hình hoá một tập trạng thái hoặc phân loại nghiệp vụ đóng kín, tính mở này trở thành lỗ hổng khiến compiler không thể bảo đảm an toàn.

Trước Java 17, để biểu diễn một tập hữu hạn các lựa chọn, lập trình viên thường dùng enum. Nhưng enum chỉ biểu diễn các hằng số đơn lẻ, không thể cho phép mỗi nhánh mang các cấu trúc dữ liệu với kiểu khác nhau. Java 17 chuẩn hoá Sealed Classes (JEP 409), mở ra kỷ nguyên lập trình hướng dữ liệu (Data-Oriented Programming) hiện đại trong Java.

1. Tại sao cây kế thừa mở tự do lại gây rủi ro cho nghiệp vụ?

Trong hệ thống quản lý mượn trả ấn phẩm của thư viện, kết quả của một giao dịch mượn sách (BorrowResult) chỉ có thể rơi vào một trong ba tình huống xác thực:

  1. Success: Mượn thành công, trả về ngày hết hạn và mã mượn.
  2. ItemReserved: Thất bại vì ấn phẩm đang được giữ chỗ bởi bạn đọc khác.
  3. AccountSuspended: Thất bại vì thẻ thư viện bị khoá, đi kèm lý do và số tiền phạt tồn đọng.

Sealed interface BorrowResult niêm phong tập con với đúng ba record Success, ItemReserved và AccountSuspended, giúp compiler kiểm tra vét cạn

Nếu BorrowResult là một interface thông thường (mở tự do), một lập trình viên khác có thể tạo thêm class PendingApproval implements BorrowResult. Toàn bộ các module thanh toán hay quầy mượn sách đang chạy sẽ hoàn toàn không biết cách xử lý lớp con mới này, dẫn đến nguy cơ rơi vào nhánh lỗi hoặc gây sụp đổ logic lúc chạy.

Sealed Interface ra đời để khoá chặt tập khả năng này:

Java
public sealed interface BorrowResult permits Success, ItemReserved, AccountSuspended {
}

public record Success(String loanId, LocalDate dueDate) implements BorrowResult { }
public record ItemReserved(String reservedByMemberId) implements BorrowResult { }
public record AccountSuspended(String reason, double outstandingFine) implements BorrowResult { }

Compiler cam kết với bạn: ngoài ba trường hợp trên, không một lớp nào khác trong toàn bộ dự án có thể tự tiện implements BorrowResult.

2. Cú pháp Sealed Class và từ khoá permits

Sealed có thể áp dụng cho cả class lẫn interface. Khi khai báo, lớp cha liệt kê danh sách subtype được phép qua từ khoá permits:

Java
public abstract sealed class LibraryAsset permits PhysicalBook, DigitalMedia, Periodical {
    private final String barcode;
    private final String title;

    protected LibraryAsset(String barcode, String title) {
        this.barcode = barcode;
        this.title = title;
    }

    public String getBarcode() { return barcode; }
    public String getTitle() { return title; }
}

Nếu một lớp khác cố tình kế thừa LibraryAsset:

Java
// Loi bien dich ngay lap tuc: class is not allowed to extend sealed class: LibraryAsset
public class CustomEquipment extends LibraryAsset {
    public CustomEquipment(String barcode, String title) {
        super(barcode, title);
    }
}

Nếu tất cả các lớp con được khai báo trong cùng một file .java với sealed parent, bạn có thể lược bỏ hoàn toàn mệnh đề permits. Compiler sẽ tự động suy luận danh sách cho phép từ các lớp con có mặt trong file đó.

3. Ba lựa chọn bắt buộc cho Subtype: final, sealed và non-sealed

Một quy tắc bất di bất dịch của Java: Mọi lớp con trực tiếp kế thừa từ sealed class/interface bắt buộc phải chọn chính xác một trong ba trạng thái kế thừa:

Từ khoá modifierÝ nghĩa thiết kếÁp dụng trong hệ thống thư viện
finalĐóng hoàn toàn cây kế thừa, không lớp nào được kế thừa tiếp.PhysicalBook: Sách in là dạng ấn phẩm cụ thể cuối cùng.
sealedTiếp tục kiểm soát chặt chẽ, chỉ cho phép một danh sách con cháu kế tiếp.DigitalMedia permits AudioBook, EbookArchive.
non-sealedMở lại cây kế thừa tự do, cho phép các bên khác mở rộng tuỳ ý.Periodical: Mở cho các viện nghiên cứu tự do bổ sung các tạp chí chuyên ngành.

Minh hoạ tổ chức cây phân cấp:

Java
// 1. final: Dong han
public final class PhysicalBook extends LibraryAsset {
    private final String shelfNumber;

    public PhysicalBook(String barcode, String title, String shelfNumber) {
        super(barcode, title);
        this.shelfNumber = shelfNumber;
    }
}

// 2. sealed: Tiep tuc kiem soat cac lop con cap 2
public abstract sealed class DigitalMedia extends LibraryAsset permits AudioBook, EbookArchive {
    protected DigitalMedia(String barcode, String title) {
        super(barcode, title);
    }
}

public final class AudioBook extends DigitalMedia {
    public AudioBook(String barcode, String title) { super(barcode, title); }
}

public final class EbookArchive extends DigitalMedia {
    public EbookArchive(String barcode, String title) { super(barcode, title); }
}

// 3. non-sealed: Mo tu do cho plugin hoac client tu extends
public non-sealed class Periodical extends LibraryAsset {
    public Periodical(String barcode, String title) {
        super(barcode, title);
    }
}

// Cac lop khac duoc phep extends Periodical tu do vi no la non-sealed
public class MedicalJournal extends Periodical {
    public MedicalJournal(String barcode, String title) { super(barcode, title); }
}

Nhờ cơ chế này, bạn có thể thiết kế kiến trúc phân tầng kết hợp (hybrid): phần cốt lõi được niêm phong để đảm bảo an toàn nghiệp vụ, trong khi các điểm mở rộng (extension points) được cởi mở bằng non-sealed.

4. Kiểm tra vét cạn (Exhaustive Pattern Matching) với Switch

Lợi ích thực tế lớn nhất của Sealed Class xuất hiện khi phối hợp cùng biểu thức switch trong Java 21:

Java
public String generateNotification(BorrowResult result) {
    return switch (result) {
        case Success s -> "Muon thanh cong! Ma: " + s.loanId() + ", han tra: " + s.dueDate();
        case ItemReserved r -> "Khong the muon: Sach da bi giu boi ma ban doc " + r.reservedByMemberId();
        case AccountSuspended a -> "The bi khoa do: " + a.reason() + ". Tien phat: " + a.outstandingFine() + " VND";
    };
}

Điểm đặc biệt ở đây: Khối switch hoàn toàn không cần mệnh đề default. Compiler biết chính xác BorrowResult chỉ có 3 thể hiện cụ thể và xác nhận rằng cả 3 trường hợp đều đã được xử lý đầy đủ.

Sự an toàn khi bảo trì và tái cấu trúc hệ thống

Hãy hình dung 6 tháng sau, nghiệp vụ thư viện mở rộng thêm tình huống: tài liệu quý hiếm chỉ được đọc tại chỗ (ReferenceOnly):

Java
public sealed interface BorrowResult permits Success, ItemReserved, AccountSuspended, ReferenceOnly {
}

public record ReferenceOnly(String readingRoom) implements BorrowResult { }

Ngay khi bạn thêm ReferenceOnly vào mệnh đề permits, hàm generateNotification và tất cả mọi vị trí switch trên BorrowResult trong toàn bộ dự án sẽ lập tức báo đỏ:

Plain Text
error: the switch expression does not cover all possible input values

Bạn không thể nào vô tình quên xử lý nghiệp vụ mới. Trình biên dịch chuyển đổi một nguy cơ lỗi nghiêm trọng lúc chạy (quên xử lý dẫn đến hành vi sai ngầm) thành lỗi biên dịch rõ ràng tại từng dòng mã.

5. Hiệu năng JVM và tính toàn vẹn bất biến

Khi tìm hiểu về sealed class, nhiều người đặt câu hỏi: "Sealed có giúp chương trình chạy nhanh hơn không?"

  1. Hiệu năng thực tế: HotSpot JVM đã thực hiện kỹ thuật Devirtualization và Inline Caching thông qua phân tích cây phân cấp (CHA) từ rất lâu trước khi Sealed Class ra đời. Do đó, việc chuyển đổi một cây kế thừa sang sealed không tạo ra bước nhảy vọt đáng kể về tốc độ thực thi. Bạn không nên dùng sealed chỉ vì kỳ vọng tối ưu micro-benchmark.
  2. Giá trị cốt lõi là Domain Invariants: Giá trị thực sự của sealed class nằm ở tính toàn vẹn kiến trúc (Architecture Integrity) và an toàn biên dịch (Compile-time Safety). Bạn tự tin khẳng định với toàn bộ hệ thống rằng dữ liệu chỉ có thể tồn tại ở những trạng thái hợp lệ đã được quy định trước.

6. So sánh Sealed Types với Enum

Tiêu chíenumsealed interface / class
Bản chấtTập hợp các hằng số định danh cố định.Tập hợp các kiểu dữ liệu đóng kín (Closed set of types).
Dữ liệu đi kèm (Payload)Mỗi hằng số mang giá trị cấu hình cố định khởi tạo sẵn.Mỗi thể hiện (instance) có thể mang dữ liệu động khác nhau.
Số lượng thể hiệnSingleton duy nhất cho mỗi hằng số.Vô số thể hiện độc lập với các thuộc tính riêng biệt.
Hỗ trợ GenericsRất hạn chế.Hỗ trợ Generics đầy đủ (sealed interface Result<T>).
Khi nào chọn?Khi mô hình hoá danh mục cố định: ngày trong tuần, vai trò người dùng (ADMIN, USER).Khi mô hình hoá kết quả nghiệp vụ, thông điệp sự kiện, hoặc trạng thái luồng làm việc.

7. Ứng dụng Sealed Class mô hình hoá State Machine

Sealed types là công cụ hoàn hảo để mô hình hoá máy trạng thái hữu hạn (Finite State Machine). Hãy quan sát vòng đời mượn sách:

Java
public sealed interface LoanLifecycle permits ActiveLoan, ReturnedOnTime, OverdueLoan, ClaimedLost {
}

public record ActiveLoan(String loanId, LocalDate borrowedAt, LocalDate dueDate) implements LoanLifecycle { }

public record ReturnedOnTime(String loanId, LocalDate returnedAt) implements LoanLifecycle { }

public record OverdueLoan(String loanId, LocalDate dueDate, int daysOverdue, double fine) implements LoanLifecycle { }

public record ClaimedLost(String loanId, double compensationCost) implements LoanLifecycle { }

Mỗi trạng thái mang theo đúng các trường thông tin cần thiết của trạng thái đó. Trạng thái ActiveLoan không cần mang trường fine (tiền phạt), và trạng thái ReturnedOnTime không cần mang trường compensationCost (tiền đền bù mất sách). Thiết kế này loại bỏ hoàn toàn các trường dữ liệu mang giá trị null vô nghĩa trong mô hình hướng đối tượng cũ.

8. Những cạm bẫy thường gặp khi sử dụng Sealed Class

❌ Bẫy 1: Quên khai báo một trong ba modifier trên lớp con

Java
public sealed class Base permits Child { }
public class Child extends Base { } // Loi bien dich!

Mỗi lớp con trực tiếp bắt buộc phải được đánh dấu rõ là final, sealed, hoặc non-sealed.

❌ Bẫy 2: Khai báo lớp con khác package hoặc khác module Tất cả các lớp được cấp phép trong mệnh đề permits phải thuộc cùng một package (nếu không dùng module) hoặc cùng một Java Module (module-info.java). Bạn không thể permit một lớp nằm ở package khác thuộc một JAR khác.

❌ Bẫy 3: Gượng ép dùng sealed cho các điểm mở rộng thư viện (Plugin API) Nếu bạn đang viết một SDK và muốn người dùng bên ngoài có thể tự do mở rộng các plugin hoặc adapter tuỳ biến, việc đánh dấu sealed ở tầng giao tiếp ngoài cùng sẽ chặn đứng khả năng tích hợp của họ. Hãy sử dụng interface mở hoặc kết hợp non-sealed tại các điểm nối cần thiết.

9. Deep Dive Oracle & OpenJDK References

Tài liệu tham khảo đặc tả
  • JEP 409 (Sealed Classes): Đặc tả chính thức đưa Sealed Classes thành tính năng hoàn thiện trong Java 17 LTS.
  • JLS §8.1.1.2 & §9.1.1.4: Các quy tắc ngữ nghĩa về Sealed Classes và Interfaces trong Java Language Specification.
  • JEP 441 (Pattern Matching for switch): Chuẩn hoá cơ chế khớp mẫu và kiểm tra vét cạn (exhaustive checking) trên Java 21 LTS.
  • Data-Oriented Programming in Java (Brian Goetz): Tài liệu từ Kiến trúc sư trưởng ngôn ngữ Java mô tả cách kết hợp Records, Sealed Types, và Pattern Matching để viết mã trong sáng, an toàn.

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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn mã sau có biên dịch thành công không và tại sao? public sealed interface StoragePolicy permits LocalDisk, CloudBlob { } public record LocalDisk(String path) implements StoragePolicy { } public class CloudBlob implements StoragePolicy { }
  2. Q2
    Tại sao khi switch trên một Sealed Type đã xử lý đủ các trường hợp thì không nên viết thêm nhánh default?
  3. Q3
    Từ khoá non-sealed giải quyết bài toán thiết kế nào trong kiến trúc phần mềm?
  4. Q4
    Lợi ích lớn nhất của việc dùng Sealed Types kết hợp Record thay cho các class có nhiều trường nullable để mô hình hoá trạng thái là gì?
  5. Q5
    Sealed Class có giúp Java Virtual Machine (JVM) tăng tốc độ thực thi đáng kể thông qua devirtualization không?

Bài tiếp theo: Composition over inheritance — khi nào không kế thừa

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 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 14/16 của chặng

Ôn phỏng vấn

Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.

Thảo luận & góp ý

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

Bài tiếp theo

Composition over inheritance — khi nào KHÔNG kế thừa