Java — Từ Zero đến Senior/Kế thừa & Đa hình/Sealed class — kiểm soát tập subtype với pattern matching exhaustive
5/7
~16 phútKế thừa & Đa hình

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

Sealed class/interface Java 17+, từ khóa permits, sub-type phải là final/sealed/non-sealed, kết hợp record + sealed + switch pattern matching cho ADT đầy đủ trong Java.

Trước Java 17, một class/interface public cho phép bất kỳ ai extend/implement nó — bạn không kiểm soát được tập subtype. Với enum thì tập cố định, nhưng enum chỉ cho các giá trị đơn — không mang field phức hợp.

Java 17 đưa ra sealed class/interface (JEP 409): class cha khai báo tường minh permits chỉ một danh sách cụ thể các subtype. Kết hợp với record và pattern matching, Java có đầy đủ công cụ của algebraic data type (ADT) — công cụ mô hình "giá trị là một trong N dạng" rất mạnh mẽ trong ngôn ngữ như Rust, Scala, Kotlin.

1. Analogy — menu nhà hàng cố định

Bạn vào quán phở — menu ghi rõ: "phở bò, phở gà, phở chay". Khách không gọi được "phở tôm" — quán không phục vụ. Bếp chuẩn bị đúng nguyên liệu cho 3 loại, không lo "phải nấu món lạ".

Đời thườngJava
Quán phởsealed interface Pho
Menu "bò, gà, chay"permits Bo, Ga, Chay
Khách gọi "phở tôm"class Tom implements Pho — compile error
Bếp biết chắc 3 loạiswitch (pho) exhaustive

💡 💡 Cách nhớ

Sealed = "class này chỉ các loại sau, không mở cho bất kỳ ai". Compiler đảm bảo tập subtype đóng kín — bạn viết switch phủ hết mà không cần default.

2. Cú pháp cơ bản

public sealed interface Shape permits Circle, Square, Triangle {
    double area();
}

public record Circle(double radius) implements Shape {
    @Override public double area() { return Math.PI * radius * radius; }
}

public record Square(double side) implements Shape {
    @Override public double area() { return side * side; }
}

public record Triangle(double base, double height) implements Shape {
    @Override public double area() { return 0.5 * base * height; }
}

Khai báo sealed interface Shape permits Circle, Square, Triangle nói với compiler: "chỉ 3 class này được implement Shape". Thêm class thứ 4:

public class Hexagon implements Shape { ... }   // COMPILE ERROR — khong trong permits

Compiler từ chối. Tập subtype đóng kínbạn kiểm soát.

2.1 Sealed class (không phải interface)

Cũng áp dụng cho class:

public sealed class Vehicle permits Car, Truck, Motorcycle {
    protected int maxSpeed;
}

public final class Car extends Vehicle { ... }
public final class Truck extends Vehicle { ... }
public non-sealed class Motorcycle extends Vehicle { ... }

3. Ba lựa chọn cho subtype — final, sealed, non-sealed

Mỗi subtype của sealed class/interface phải chọn 1 trong 3 trạng thái về kế thừa:

Trạng tháiÝ nghĩa
finalKhông ai extends subtype này — đóng hoàn toàn
sealedSubtype tiếp tục sealed — permits 1 danh sách cụ thể
non-sealedSubtype "mở ra" — ai cũng extends được
public sealed interface Shape permits Circle, Ellipse, FreeShape { }

public record Circle(double r) implements Shape { }          // record ngam final

public sealed interface Ellipse extends Shape permits Oval { }  // tiep tuc sealed
public record Oval(double a, double b) implements Ellipse { }

public non-sealed interface FreeShape extends Shape { }      // mo — ai cung extends
public class SvgPath implements FreeShape { }                 // OK vi FreeShape non-sealed
public class BezierCurve implements FreeShape { }             // OK

3 lựa chọn cho phép thiết kế hierarchy hybrid: phần lõi đóng (compiler check exhaustive), một vài nhánh mở (cho user extend).

4. Cùng file hoặc cùng module

Subtype của sealed phải khai báo cùng module (nếu có module system) hoặc cùng package. Cú pháp ngắn gọn: đặt hết trong cùng file và bỏ permits:

// Shape.java
public sealed interface Shape {
    double area();
}
record Circle(double radius) implements Shape {
    @Override public double area() { return Math.PI * radius * radius; }
}
record Square(double side) implements Shape {
    @Override public double area() { return side * side; }
}
record Triangle(double base, double height) implements Shape {
    @Override public double area() { return 0.5 * base * height; }
}

Compiler tự suy permits = các subtype trong cùng file. Thuận tiện cho ADT nhỏ.

5. Pattern matching exhaustive — giá trị lớn nhất

Với sealed + switch pattern matching (Java 21+):

public static double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square sq -> sq.side() * sq.side();
        case Triangle t -> 0.5 * t.base() * t.height();
        // KHONG can default — compiler biet 3 case day du
    };
}

Compiler exhaustive check: phủ đủ 3 subtype → OK. Thiếu case → compile error "the switch expression does not cover all possible input values".

Khi bạn thêm subtype mới vào permits, mọi switch chưa phủ case đó compile error ngay — bạn không thể quên.

// Them vao sealed:
public sealed interface Shape permits Circle, Square, Triangle, Pentagon { }

// Moi switch cu:
switch (s) {
    case Circle c -> ...;
    case Square sq -> ...;
    case Triangle t -> ...;
    // COMPILE ERROR — chua phu Pentagon
}

Đây là ưu điểm khổng lồ so với if/else if/else hay switch không exhaustive — refactor an toàn, compiler tự bắt lỗi.

6. Ví dụ thực tế — Result type

Pattern ADT kinh điển: biểu diễn "thành công hoặc lỗi":

public sealed interface Result<T> permits Success, Failure { }

public record Success<T>(T value) implements Result<T> { }
public record Failure<T>(String reason) implements Result<T> { }

// Su dung
Result<Integer> parseNumber(String s) {
    try {
        return new Success<>(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return new Failure<>("not a number: " + s);
    }
}

// Consume
Result<Integer> r = parseNumber("42");
switch (r) {
    case Success<Integer> s -> System.out.println("got: " + s.value());
    case Failure<Integer> f -> System.out.println("error: " + f.reason());
}

Không cần null, không cần exception cho flow bình thường, compiler đảm bảo caller xử lý cả 2 case.

7. Sealed vs enum — khi nào chọn cái nào?

enumsealed
Số lượng giá trịCố định số lượng (VD 7 ngày)Cố định số loại, mỗi loại có instance tuỳ
Field phức hợpCó (enum có field)Có (record/class)
GenericHạn chế✅ đầy đủ
Pattern matchingswitch valueswitch type + deconstruction
Dùng khiTập hữu hạn giá trị rờiTập hữu hạn kiểu với dữ liệu riêng
enum Direction { NORTH, SOUTH, EAST, WEST }   // 4 gia tri co dinh

sealed interface Shape permits Circle, Square { }
record Circle(double r) implements Shape { }
record Square(double s) implements Shape { }
// Moi "loai" co field rieng, so luong instance vo han (voi radius/side khac nhau)

8. Sealed cho state machine

public sealed interface OrderState permits Pending, Confirmed, Shipped, Delivered, Cancelled { }

public record Pending() implements OrderState { }
public record Confirmed(LocalDateTime confirmedAt) implements OrderState { }
public record Shipped(String trackingNumber, LocalDateTime shippedAt) implements OrderState { }
public record Delivered(LocalDateTime deliveredAt) implements OrderState { }
public record Cancelled(String reason) implements OrderState { }

public static String displayStatus(OrderState state) {
    return switch (state) {
        case Pending p -> "Waiting for confirmation";
        case Confirmed c -> "Confirmed at " + c.confirmedAt();
        case Shipped s -> "Shipping, track: " + s.trackingNumber();
        case Delivered d -> "Delivered at " + d.deliveredAt();
        case Cancelled x -> "Cancelled: " + x.reason();
    };
}

Mỗi state mang data phù hợp với state: Confirmed có timestamp confirm, Shipped có tracking number. Không có state nào mang field dư thừa ("null field" bug class).

9. Pitfall tổng hợp

Nhầm 1: Subtype không chọn final/sealed/non-sealed.

sealed interface Shape permits Circle { }
class Circle implements Shape { }   // COMPILE ERROR

✅ Chọn final class Circle, hoặc sealed class Circle permits ..., hoặc non-sealed class Circle.

Nhầm 2: Record quên — record ngầm final.

sealed interface Shape permits Circle { }
record Circle(double r) implements Shape { }   // OK — record la final

✅ OK, record không cần (và không thể) khai final lại.

Nhầm 3: Subtype khai báo khác package mà không module.

// pkgA/Shape.java
sealed interface Shape permits pkgB.Circle { }

// pkgB/Circle.java
record Circle(double r) implements pkgA.Shape { }

✅ Cùng package (hoặc cùng module) — đơn giản: đặt trong cùng file hoặc tách thành library có module-info.

Nhầm 4: Thêm subtype mới mà quên update switch.

// Trước:
sealed interface Shape permits Circle, Square { }
// Sau:
sealed interface Shape permits Circle, Square, Triangle { }

// Switch cu chi phu 2 case -> COMPILE ERROR tu dong

✅ Đây là feature, không phải bug. Cảm ơn compiler.

Nhầm 5: Dùng sealed cho hierarchy mở cần extend library ngoài.

// Library: sealed interface Event permits UserEvent, OrderEvent;
// User khong the add CustomEvent implements Event

✅ Nếu muốn user extend, đừng sealed — dùng interface thường hoặc non-sealed.

10. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: Brian Goetz mô tả sealed là "primitive essential cho algebraic data type trong Java". Kết hợp sealed + record + switch pattern matching = triết lý "data-oriented programming" (blog Goetz) — đối lập với "object-oriented" truyền thống. Dùng nhiều ở domain modeling, parsing, state machine — những nơi "giá trị là một trong N dạng" có lợi hơn "object với behaviour".

11. Tóm tắt

  • Sealed (Java 17+) = class/interface khai permits X, Y, Z — chỉ 3 class này được extends/implements.
  • Subtype phải chọn 1 trong 3: final, sealed, non-sealed.
  • final — đóng hẳn; sealed — tiếp tục kiểm soát; non-sealed — mở cho extend tự do.
  • Subtype phải cùng module (hoặc cùng package nếu không có module). Cùng file thì bỏ được permits.
  • Record ngầm final — hợp hoàn hảo với sealed.
  • Sealed + record + switch pattern matching (Java 21) = ADT đầy đủ: compiler kiểm tra exhaustive, thêm subtype bắt lỗi tự động mọi switch chưa phủ.
  • Dùng cho: shape hierarchy, Result/Either type, state machine, AST node, event catalog.
  • So với enum: sealed cho mỗi "dạng" mang data phức hợp, không chỉ giá trị đơn.
  • Đừng sealed nếu muốn library cho user extend.

12. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau compile không?
public sealed interface Shape permits Circle, Square { }
public class Circle implements Shape { double radius; }
public record Square(double side) implements Shape { }

Square OK (record ngầm final). Nhưng Circle compile error.

Rule: subtype của sealed phải chọn 1 trong 3: final, sealed, non-sealed. class Circle không có modifier nào trong 3 → compiler từ chối.

Fix (chọn 1):

  • public final class Circle implements Shape — đóng hẳn, không ai extends Circle được.
  • public non-sealed class Circle implements Shape — mở cho subclass tự do.
  • public sealed class Circle implements Shape permits RedCircle, BlueCircle — tiếp tục kiểm soát.
  • Hoặc đổi Circle thành record Circle(double radius) — record ngầm final.
Q2
Viết sealed interface Result với 2 subtype (Success, Failure), rồi method describe(Result r) dùng switch exhaustive.
public sealed interface Result<T> permits Success, Failure { }

public record Success<T>(T value) implements Result<T> { }
public record Failure<T>(String reason) implements Result<T> { }

public static String describe(Result<?> r) {
    return switch (r) {
        case Success<?> s -> "OK: " + s.value();
        case Failure<?> f -> "ERR: " + f.reason();
    };
}

// Test:
Result<Integer> ok = new Success<>(42);
Result<Integer> err = new Failure<>("timeout");
System.out.println(describe(ok));   // OK: 42
System.out.println(describe(err));  // ERR: timeout

Ghi chú:

  • 2 subtype là record → ngầm final, hợp rule sealed.
  • Switch không có default — compiler biết 2 case phủ đủ. Thêm case Pending vào Result sẽ compile error ở switch này.
  • Generic <T>: trong switch dùng wildcard <?> khi không cần bind generic type cụ thể.
Q3
Vì sao non-sealed tồn tại? Cho ví dụ use case hợp lý.

non-sealed cho phép "sealed có một lỗ hổng" — phần lớn hierarchy đóng, một vài nhánh mở.

Use case: framework/library thiết kế plugin point. Library giữ tập lõi đóng (biết chắc để tối ưu, kiểm exhaustive), nhưng cho user tự extend ở 1 điểm:

// Library core:
public sealed interface HttpRequest permits GetRequest, PostRequest, CustomRequest { }

public record GetRequest(String url) implements HttpRequest { }
public record PostRequest(String url, byte[] body) implements HttpRequest { }

// Mo cho user extend:
public non-sealed interface CustomRequest extends HttpRequest {
    String method();
    String url();
}

// User:
public class PatchRequest implements CustomRequest {
    public String method() { return "PATCH"; }
    public String url() { return "/api/update"; }
}

Lợi ích: library biết rõ 2 case chính (GetRequest, PostRequest) và handle tối ưu, user có thể thêm method HTTP tuỳ ý qua CustomRequest mà không phá hệ thống.

Switch phải có default để xử lý CustomRequest vì tập mở:

switch (req) {
    case GetRequest g -> handleGet(g);
    case PostRequest p -> handlePost(p);
    case CustomRequest c -> handleCustom(c);  // non-sealed -> default-like
}
Q4
Khi nào nên chọn sealed thay vì enum, và ngược lại?

Enum khi:

  • Tập giá trị rời rạc cố định — vd 7 ngày, 4 hướng, 3 log level.
  • Mỗi giá trị là singleton, không có instance riêng với data.
  • Cần so sánh với == an toàn (enum value là singleton).
  • Cần dùng với EnumSet/EnumMap — cấu trúc tối ưu.

Sealed khi:

  • Tập kiểu cố định, mỗi kiểu mang data riêng: Result (Success có value, Failure có reason).
  • Muốn mỗi kiểu có nhiều instance với field khác nhau: Circle(5)Circle(10) là 2 object.
  • Kết hợp với pattern matching deconstruction.
  • Cần generic: sealed interface Result<T> — enum không làm được đầy đủ.
  • State machine với dữ liệu phụ thuộc state: ShippedtrackingNumber, Pending không.

Rule: "giá trị rời" → enum; "kiểu rời, mỗi kiểu mang data" → sealed. Với tiếng Anh: enum hợp "one of these constants", sealed hợp "one of these kinds".

Q5
Ưu điểm lớn nhất của sealed + pattern matching so với chain if (x instanceof A) ... else if (x instanceof B) ... là gì?

Exhaustive check tại compile time.

Với if/else if, bạn phải nhớ thêm branch mỗi khi tạo kiểu mới — quên thì compiler không báo, chỉ biết khi runtime rơi vào else cuối hoặc ném IllegalStateException. Refactor đau.

Với sealed + switch pattern matching:

  • Compiler biết tập subtype (thông qua permits).
  • Switch không có default phải phủ mọi subtype — compile error nếu thiếu.
  • Thêm subtype mới vào permitsmọi switch chưa phủ case mới lập tức compile error. IDE chỉ thẳng vị trí cần update.
  • Không bao giờ "quên branch" và có bug runtime im lặng.

Lợi ích khác: deconstruction. case Success<Integer>(Integer value) -> use(value) trích field trực tiếp vào biến, không phải ((Success) r).value().

Tóm lại: sealed + pattern matching đổi bug runtime thành compile error — rẻ hơn nhiều.


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