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

Sealed Classes

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

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

  • Hiểu Sealed Classes (Java 17+) — kiểm soát chặt chẽ phân cấp kế thừa (inheritance hierarchy)
  • Biết cách dùng sealed, permits, non-sealed keywords
  • Nắm được 3 options cho subclasses: final, sealed, non-sealed
  • Kết hợp sealed classes với pattern matching cho kiểm tra đầy đủ (exhaustive switch)
  • Áp dụng sealed classes cho domain modeling và type-safe hierarchies

Bài trước: Records — Đã học về immutable data carriers. Bài này sẽ tìm hiểu Sealed Classes — kiểm soát ai được phép extend class của bạn.

Giới thiệu

Sealed classes được giới thiệu như preview feature trong Java 15, và trở thành standard feature trong Java 17 (September 2021). Sealed classes cho phép bạn kiểm soát chặt chẽ phân cấp kế thừa (inheritance hierarchy) — xác định class/interface nào được phép extend hoặc implement.

Hành trình của Sealed Classes

VersionStatusJEP
Java 15PreviewJEP 360
Java 16Second PreviewJEP 397
Java 17StandardJEP 409
Java 21Kết hợp Pattern Matching for switchJEP 441

Sealed classes mất 3 phiên bản từ preview đến standard — cho thấy Java Team cẩn trọng trong thiết kế.

Vấn đề Sealed Classes giải quyết

Trước Sealed Classes

Với class/interface thông thường, bạn chỉ có 2 lựa chọn:

// Option 1: public class - ANYONE có thể extend
public class Shape {
// ...
}
// Bất kỳ ai cũng có thể:
class Triangle extends Shape {}
class Pentagon extends Shape {}
// Không kiểm soát được!

// Option 2: final class - KHÔNG AI được extend
public final class Circle {
// ...
}
// Không thể extend, mất tính linh hoạt

Vấn đề: Không có cách nào để nói "chỉ cho phép 3 subclasses này, không cho phép thêm nữa".

Với Sealed Classes

// Chỉ cho phép Circle, Rectangle, Square extend
public sealed class Shape
permits Circle, Rectangle, Square {
// ...
}

// OK - Được liệt kê trong permits
final class Circle extends Shape {}
final class Rectangle extends Shape {}
final class Square extends Shape {}

// COMPILE ERROR - Không được phép
class Pentagon extends Shape {} // ERROR!

Cú pháp Sealed Classes

sealed và permits keywords

// Sealed class
public sealed class Shape
permits Circle, Rectangle, Square {

public abstract double area();
}

// Permitted subclasses
final class Circle extends Shape {
private final double radius;

public Circle(double radius) {
this.radius = radius;
}

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

final class Rectangle extends Shape {
private final double width, height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

@Override
public double area() {
return width * height;
}
}

final class Square extends Shape {
private final double side;

public Square(double side) {
this.side = side;
}

@Override
public double area() {
return side * side;
}
}

Subclass options: final, sealed, non-sealed

Mỗi permitted subclass PHẢI chọn 1 trong 3 options:

ModifierÝ nghĩaUse case
finalKhông thể extend thêmLeaf class trong hierarchy
sealedCó thể extend, nhưng kiểm soátIntermediate class trong hierarchy
non-sealedMở cho inheritance bình thườngCho phép extension tự do
public sealed class Vehicle
permits Car, Truck, Motorcycle {
// ...
}

// Option 1: final - Không thể extend
final class Car extends Vehicle {
// Không ai extend Car được
}

// Option 2: sealed - Kiểm soát tiếp
sealed class Truck extends Vehicle
permits PickupTruck, SemiTruck {
// Chỉ PickupTruck và SemiTruck extend Truck được
}

// Option 3: non-sealed - Mở lại
non-sealed class Motorcycle extends Vehicle {
// Bất kỳ ai cũng extend Motorcycle được
}

// Subclasses của Truck
final class PickupTruck extends Truck {}
final class SemiTruck extends Truck {}

// OK - Motorcycle là non-sealed
class Scooter extends Motorcycle {}
class SportBike extends Motorcycle {}
Quy tắc chọn modifier
  • final: Khi không cần extend thêm (phổ biến nhất)
  • sealed: Khi cần phân cấp nhiều level
  • non-sealed: Khi muốn mở lại inheritance từ điểm này

Sealed Interfaces

Interfaces cũng có thể sealed:

public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {

int eval();
}

// Permitted implementations
record ConstantExpr(int value) implements Expr {
@Override
public int eval() {
return value;
}
}

record PlusExpr(Expr left, Expr right) implements Expr {
@Override
public int eval() {
return left.eval() + right.eval();
}
}

record TimesExpr(Expr left, Expr right) implements Expr {
@Override
public int eval() {
return left.eval() * right.eval();
}
}

record NegExpr(Expr expr) implements Expr {
@Override
public int eval() {
return -expr.eval();
}
}

// Usage
Expr expr = new PlusExpr(
new ConstantExpr(5),
new TimesExpr(
new ConstantExpr(3),
new ConstantExpr(2)
)
);
System.out.println(expr.eval()); // 5 + (3 * 2) = 11

Kiểm tra đầy đủ (Exhaustiveness Checking) trong Switch

Một trong những lợi ích lớn nhất của sealed classes là kiểm tra đầy đủ (exhaustiveness checking) trong switch expressions (Java 17+):

Without Sealed Classes

// Class bình thường - cần default case
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
// PHẢI có default vì có thể có subclass khác
default -> throw new IllegalArgumentException("Unknown shape");
};
}

With Sealed Classes

// Sealed class - KHÔNG cần default case
public sealed interface Shape permits Circle, Rectangle, Square {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Square(double side) implements Shape {}

public double calculateArea(Shape shape) {
// Compiler biết chỉ có 3 cases, không cần default!
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Square s -> s.side() * s.side();
// Không cần default!
};
}

// Nếu thêm Triangle vào permits, compiler sẽ BẮT phải thêm case Triangle
Exhaustiveness = Type safety

Compiler đảm bảo bạn xử lý TẤT CẢ cases. Nếu thêm permitted subclass mới, tất cả switch expressions sẽ compile error cho đến khi bạn thêm case mới.

Sealed Classes + Records = Kiểu dữ liệu đại số

Kết hợp sealed classes với records tạo nên kiểu dữ liệu đại số (Algebraic Data Types - ADTs) như trong Scala, Haskell:

Algebraic Data Type Pattern với Sealed + Records

Ví dụ: Result type (thay Optional)

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

// Static factory methods
static <T> Result<T> success(T value) {
return new Success<>(value);
}

static <T> Result<T> failure(String error) {
return new Failure<>(error);
}
}

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

// Usage
public Result<User> findUser(long id) {
User user = repository.findById(id);
if (user != null) {
return Result.success(user);
} else {
return Result.failure("User not found: " + id);
}
}

// Pattern matching
Result<User> result = findUser(123);
String message = switch (result) {
case Success<User> s -> "Found user: " + s.value().getName();
case Failure<User> f -> "Error: " + f.error();
};

Ví dụ: JSON values

public sealed interface JsonValue
permits JsonObject, JsonArray, JsonString,
JsonNumber, JsonBoolean, JsonNull {
}

record JsonObject(Map<String, JsonValue> fields) implements JsonValue {}
record JsonArray(List<JsonValue> elements) implements JsonValue {}
record JsonString(String value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonBoolean(boolean value) implements JsonValue {}
record JsonNull() implements JsonValue {}

// Parser
public String toStringValue(JsonValue json) {
return switch (json) {
case JsonString s -> s.value();
case JsonNumber n -> String.valueOf(n.value());
case JsonBoolean b -> String.valueOf(b.value());
case JsonNull n -> "null";
case JsonObject o -> o.fields().toString();
case JsonArray a -> a.elements().toString();
};
}

So sánh với ngôn ngữ khác

Java sealed + records đạt được hiệu quả tương đương các ngôn ngữ khác:

// Kotlin sealed class
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
}
// Rust enum (discriminated union)
enum Result<T> {
Success(T),
Failure(String),
}
// Java sealed interface + records
sealed interface Result<T> permits Success, Failure {}
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}

Ba cách viết trên biểu đạt cùng một ý tưởng: kiểu hợp phân biệt (discriminated union) — một giá trị chỉ có thể thuộc một trong các biến thể đã định trước.

Ví dụ: Shape Hierarchy

Traditional approach (không sealed)

public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}

class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }

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

@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}

// Vấn đề: Bất kỳ ai cũng tạo subclass mới
class Ellipse extends Shape { ... } // Uncontrolled!

Sealed approach

public sealed interface Shape
permits Circle, Rectangle, Triangle {

double area();
double perimeter();
}

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

@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}

record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}

@Override
public double perimeter() {
return 2 * (width + height);
}
}

record Triangle(double a, double b, double c) implements Shape {
@Override
public double area() {
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}

@Override
public double perimeter() {
return a + b + c;
}
}

### Sự kết hợp hoàn hảo: Sealed + Records + Pattern Matching

Sealed classes kết hợp với records và pattern matching tạo nên một hệ sinh thái hoàn chỉnh:

```mermaid
flowchart TD
S["sealed interface Shape&lt;br/&gt;permits Circle, Rectangle, Triangle"]
S --> R1["record Circle(double radius)"]
S --> R2["record Rectangle(double w, double h)"]
S --> R3["record Triangle(double a, double b, double c)"]

R1 --> PM["Pattern Matching Switch&lt;br/&gt;(exhaustive — không cần default)"]
R2 --> PM
R3 --> PM

PM --> D1["case Circle(var r) → π×r²"]
PM --> D2["case Rectangle(var w, var h) → w×h"]
PM --> D3["case Triangle(var a, var b, var c) → Heron"]

style S fill:#e1f5fe,stroke:#0288d1
style R1 fill:#e8f5e9,stroke:#388e3c
style R2 fill:#e8f5e9,stroke:#388e3c
style R3 fill:#e8f5e9,stroke:#388e3c
style PM fill:#fff3e0,stroke:#f57c00
style D1 fill:#fce4ec,stroke:#c62828
style D2 fill:#fce4ec,stroke:#c62828
style D3 fill:#fce4ec,stroke:#c62828
// Usage với exhaustive switch
public void printShapeInfo(Shape shape) {
String description = switch (shape) {
case Circle c ->
"Circle with radius " + c.radius();
case Rectangle r ->
"Rectangle " + r.width() + "x" + r.height();
case Triangle t ->
"Triangle with sides " + t.a() + ", " + t.b() + ", " + t.c();
};

System.out.println(description);
System.out.println("Area: " + shape.area());
System.out.println("Perimeter: " + shape.perimeter());
}

Khi nào dùng Sealed Classes

1. Domain Modeling

// Payment methods
public sealed interface PaymentMethod
permits CreditCard, DebitCard, PayPal, BankTransfer {
}

record CreditCard(String number, String cvv, String expiry)
implements PaymentMethod {}
record DebitCard(String number, String pin)
implements PaymentMethod {}
record PayPal(String email)
implements PaymentMethod {}
record BankTransfer(String accountNumber, String routingNumber)
implements PaymentMethod {}

// Process payment với exhaustive checking
public void processPayment(PaymentMethod method, double amount) {
switch (method) {
case CreditCard cc -> processCreditCard(cc, amount);
case DebitCard dc -> processDebitCard(dc, amount);
case PayPal pp -> processPayPal(pp, amount);
case BankTransfer bt -> processBankTransfer(bt, amount);
}
}

2. State Machines

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

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

// State transitions
public OrderState processEvent(OrderState current, OrderEvent event) {
return switch (current) {
case Pending p -> switch (event) {
case PaymentReceived pr ->
new Confirmed(LocalDateTime.now(), pr.paymentId());
case OrderCancelled oc ->
new Cancelled(LocalDateTime.now(), oc.reason());
default -> current;
};
case Confirmed c -> switch (event) {
case OrderShipped os ->
new Shipped(LocalDateTime.now(), os.trackingNumber());
default -> current;
};
// ... other states
};
}

3. AST (Abstract Syntax Trees)

// Expression AST
public sealed interface Expr
permits Literal, Variable, BinaryOp, UnaryOp {
}

record Literal(Object value) implements Expr {}
record Variable(String name) implements Expr {}
record BinaryOp(String operator, Expr left, Expr right) implements Expr {}
record UnaryOp(String operator, Expr operand) implements Expr {}

// Evaluator
public Object eval(Expr expr, Map<String, Object> env) {
return switch (expr) {
case Literal lit -> lit.value();
case Variable var -> env.get(var.name());
case BinaryOp op -> evalBinaryOp(op, env);
case UnaryOp op -> evalUnaryOp(op, env);
};
}

4. API Responses/Events

// API Response types
public sealed interface ApiResponse
permits SuccessResponse, ErrorResponse, ValidationErrorResponse {
}

record SuccessResponse(Object data, String message)
implements ApiResponse {}
record ErrorResponse(int code, String message)
implements ApiResponse {}
record ValidationErrorResponse(Map<String, List<String>> errors)
implements ApiResponse {}

// HTTP status code mapping
public int getStatusCode(ApiResponse response) {
return switch (response) {
case SuccessResponse s -> 200;
case ErrorResponse e -> e.code();
case ValidationErrorResponse v -> 400;
};
}

5. Xử lý lỗi có cấu trúc (Structured Error Handling)

sealed interface AppError permits ValidationError, NotFoundError,
AuthError, InternalError {}

record ValidationError(String field, String message) implements AppError {}
record NotFoundError(String resource, String id) implements AppError {}
record AuthError(String reason) implements AppError {}
record InternalError(Exception cause) implements AppError {}

// Xử lý lỗi thống nhất
public ResponseEntity<?> handleError(AppError error) {
return switch (error) {
case ValidationError(var field, var msg) ->
ResponseEntity.badRequest()
.body(Map.of("field", field, "error", msg));
case NotFoundError(var resource, var id) ->
ResponseEntity.status(404)
.body(Map.of("message", resource + " not found: " + id));
case AuthError(var reason) ->
ResponseEntity.status(401)
.body(Map.of("error", reason));
case InternalError(var cause) -> {
log.error("Internal error", cause);
yield ResponseEntity.status(500)
.body(Map.of("error", "Internal server error"));
}
};
}

Mẫu này an toàn hơn exception-based error handling vì compiler bắt buộc xử lý mọi trường hợp lỗi.

Same Package Requirement

Sealed classes và permitted subclasses PHẢI ở cùng package (hoặc cùng module):

// package com.example.shapes;
public sealed class Shape
permits Circle, Rectangle { // Phải cùng package
// ...
}

// OK - Cùng package
// package com.example.shapes;
final class Circle extends Shape { ... }

// COMPILE ERROR - Khác package
// package com.example.other;
final class Triangle extends Shape { ... } // ERROR!
Implicit permits (cùng file)

Nếu sealed class và permitted subclasses cùng file, có thể bỏ permits:

// Shape.java
public sealed class Shape { // Không cần permits
// ...
}

// Cùng file
final class Circle extends Shape {}
final class Rectangle extends Shape {}

Sealed Classes và Modules

Sealed classes có quy tắc nghiêm ngặt về vị trí:

Unnamed modules (không dùng module-info.java)

Permitted subclasses PHẢI ở cùng package:

// ✅ Cùng package com.example.model
package com.example.model;

public sealed class Shape permits Circle, Rectangle {}
final class Circle extends Shape {} // OK - cùng package
final class Rectangle extends Shape {} // OK - cùng package

// ❌ Khác package → Compile Error
package com.example.other;
final class Triangle extends Shape {} // ERROR!

Named modules (dùng module-info.java)

Permitted subclasses PHẢI ở cùng module (có thể khác package):

// module-info.java
module com.example.shapes {
exports com.example.shapes.api;
exports com.example.shapes.impl;
}

// ✅ Cùng module, khác package
package com.example.shapes.api;
public sealed class Shape
permits com.example.shapes.impl.Circle,
com.example.shapes.impl.Rectangle {}

package com.example.shapes.impl;
public final class Circle extends Shape {} // OK - cùng module
public final class Rectangle extends Shape {} // OK - cùng module

Tại sao? Sealed classes đảm bảo compile-time exhaustiveness. Nếu subclasses ở module khác, compiler không thể verify tất cả subclasses tại compile time.

Sealed Classes thay thế Visitor Pattern

Trước sealed classes, Visitor Pattern là cách phổ biến để thêm operations cho class hierarchy mà không sửa các class gốc. Sealed classes + pattern matching thay thế hoàn toàn:

Truyền thống: Visitor Pattern

// Visitor Pattern - Verbose, boilerplate nhiều
interface ShapeVisitor<R> {
R visitCircle(Circle circle);
R visitRectangle(Rectangle rectangle);
R visitTriangle(Triangle triangle);
}

abstract class Shape {
abstract <R> R accept(ShapeVisitor<R> visitor);
}

class Circle extends Shape {
double radius;
@Override
<R> R accept(ShapeVisitor<R> visitor) {
return visitor.visitCircle(this);
}
}
// ... Rectangle, Triangle tương tự

// Mỗi operation = 1 Visitor implementation
class AreaCalculator implements ShapeVisitor<Double> {
@Override
public Double visitCircle(Circle c) { return Math.PI * c.radius * c.radius; }
@Override
public Double visitRectangle(Rectangle r) { return r.width * r.height; }
@Override
public Double visitTriangle(Triangle t) { /* ... */ return 0.0; }
}

Modern: Sealed + Pattern Matching

// Sealed + Pattern matching — Clean, readable
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double a, double b, double c) implements Shape {}

// Mỗi operation = 1 method với switch
double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var a, var b, var c) -> {
double s = (a + b + c) / 2;
yield Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
};
}

String describe(Shape shape) {
return switch (shape) {
case Circle(var r) -> "Hình tròn bán kính " + r;
case Rectangle(var w, var h) -> "HCN " + w + "×" + h;
case Triangle(var a, var b, var c) -> "Tam giác " + a + ", " + b + ", " + c;
};
}
Tiêu chíVisitor PatternSealed + Pattern Matching
BoilerplateRất nhiều (accept, visit methods)Rất ít
Thêm operation mớiThêm Visitor mớiThêm method với switch
Thêm subtype mớiSửa tất cả VisitorsCompiler báo lỗi tất cả switch chưa exhaustive
Type safetyRuntime (có thể quên visit method)Compile-time (exhaustiveness)
ReadabilityKhó theo dõi flowDễ đọc, sequential

Sealed Classes vs Enums

Đặc điểmSealed ClassesEnums
ValuesUnlimited instancesFixed constants
DataMỗi subclass có fields riêngTất cả constants cùng fields
MethodsMỗi subclass có methods riêngShared methods
PolymorphismFull OOPLimited
ExtensibilityCompile-time controlledKhông extend được
// ENUM - Fixed constants
enum PaymentMethod {
CREDIT_CARD, DEBIT_CARD, PAYPAL;
// Tất cả constants giống nhau
}

// SEALED CLASS - Flexible
sealed interface PaymentMethod
permits CreditCard, DebitCard, PayPal {}
record CreditCard(String number, String cvv) implements PaymentMethod {}
record DebitCard(String number, String pin) implements PaymentMethod {}
record PayPal(String email) implements PaymentMethod {}
// Mỗi implementation có structure riêng

Khi nào dùng Sealed Class, Enum, hay Abstract Class?

Sealed Abstract Classes

public sealed abstract class Command
permits CreateCommand, UpdateCommand, DeleteCommand {

protected final long entityId;

protected Command(long entityId) {
this.entityId = entityId;
}

public abstract void execute();
}

final class CreateCommand extends Command {
private final Map<String, Object> data;

public CreateCommand(long entityId, Map<String, Object> data) {
super(entityId);
this.data = data;
}

@Override
public void execute() {
System.out.println("Creating entity " + entityId + " with " + data);
}
}

final class UpdateCommand extends Command {
private final Map<String, Object> updates;

public UpdateCommand(long entityId, Map<String, Object> updates) {
super(entityId);
this.updates = updates;
}

@Override
public void execute() {
System.out.println("Updating entity " + entityId + " with " + updates);
}
}

final class DeleteCommand extends Command {
public DeleteCommand(long entityId) {
super(entityId);
}

@Override
public void execute() {
System.out.println("Deleting entity " + entityId);
}
}

Performance

Performance

Sealed classes không có runtime overhead. Compiler và JVM có thể optimize tốt hơn vì biết tất cả possible subclasses:

  • Devirtualization: JIT compiler có thể inline method calls
  • Memory layout: Better cache locality
  • Switch statements: Optimized dispatch

Bên trong hoạt động ra sao

permits trong bytecode

Khi compiler xử lý sealed class, nó thêm thuộc tính PermittedSubclasses vào bytecode:

// javap -verbose Shape.class (simplified)
public sealed class Shape
PermittedSubclasses: Circle, Rectangle, Square

JVM sử dụng thuộc tính này tại class loading time để kiểm tra: khi một class cố gắng extend Shape, class loader xác nhận class đó có trong danh sách permitted hay không.

Cơ chế kiểm tra đầy đủ (Exhaustiveness)

Compiler kiểm tra exhaustiveness theo thuật toán:

  1. Lấy danh sách PermittedSubclasses từ sealed type
  2. Với mỗi final subclass → phải có case tương ứng
  3. Với mỗi sealed subclass → đệ quy kiểm tra các permitted subclass con
  4. Với non-sealed subclass → KHÔNG THỂ kiểm tra đầy đủ → cần default
sealed interface A permits B, C {}
sealed interface B extends A permits D, E {}
final class C implements A {}
final class D implements B {}
final class E implements B {}

// Compiler biết exhaustive cases là: C, D, E (không phải A, B)
String result = switch (a) {
case C c -> "C";
case D d -> "D";
case E e -> "E";
// Không cần default!
};
OCP Exam Tips
  • Permitted subclass PHẢI ở cùng module (hoặc cùng package nếu unnamed module) với sealed class
  • Mỗi permitted subclass PHẢI chọn đúng 1 trong 3: final, sealed, hoặc non-sealed
  • non-sealed cho phép mở rộng tiếp — phá vỡ exhaustiveness checking (switch cần default)
  • Nếu sealed class và tất cả subclass cùng file → có thể bỏ permits clause
  • Sealed interface cho phép cả classrecord implement
  • Record implement sealed interface → ngầm hiểu là final (không cần viết final record)
  • Switch exhaustiveness chỉ hoạt động khi TẤT CẢ lá trong cây kế thừa là final hoặc sealed
OCP Trap — non-sealed phá vỡ exhaustiveness

Khi một permitted subclass là non-sealed, bất kỳ ai cũng có thể extend nó. Compiler KHÔNG THỂ kiểm tra đầy đủ → switch BẮT BUỘC cần default:

sealed interface Animal permits Dog, Cat {}
final class Dog implements Animal {}
non-sealed class Cat implements Animal {}
class PersianCat extends Cat {} // OK vì Cat là non-sealed

String sound(Animal a) {
return switch (a) {
case Dog d -> "Woof";
case Cat c -> "Meow";
// ❌ Compile Error nếu không có default!
// Vì PersianCat (hoặc bất kỳ subclass nào của Cat)
// có thể không match "case Cat"
// ✅ Phải thêm:
default -> "Unknown";
};
}

Nhưng nếu tất cả subclasses là final hoặc sealedKHÔNG cần default.

OCP Trap — Record là ngầm final khi implement sealed interface

Record implicitly final — không cần (và không được) viết final record:

sealed interface Shape permits Circle, Rectangle {}

record Circle(double radius) implements Shape {} // ✅ implicitly final
final record Rectangle(double w, double h) implements Shape {} // ❌ Compile Error!
// "Modifier 'final' not allowed here"

Exam hay hỏi: "Record nào compile?" — nhớ record đã final, không cần viết thêm.

📖 Theo JLS §8.1.1.2 — sealed Classes
  • Sealed class khai báo permits clause liệt kê tất cả permitted direct subclasses
  • Mỗi permitted subclass phải extend/implement sealed type trực tiếp
  • Mỗi permitted subclass phải là final, sealed, hoặc non-sealed
  • Permitted subclasses phải accessible tại compile time (cùng module hoặc cùng package)
  • JVM enforce sealed constraint tại class loading time qua PermittedSubclasses attribute

Tham khảo: JLS §8.1.1.2

Kết luận

Khi nào dùng Sealed Classes

NÊN dùng khi:

  • Muốn kiểm soát chặt chẽ phân cấp kế thừa (inheritance hierarchy)
  • Domain có fixed set of types (payment methods, states, shapes, etc.)
  • Cần kiểm tra đầy đủ (exhaustiveness checking) trong switch
  • Modeling kiểu dữ liệu đại số (algebraic data types)

KHÔNG nên dùng khi:

  • Cần extensibility - third parties có thể extend
  • Không biết trước tất cả subtypes
  • Simple enums đã đủ

Bài tập thực hành

Bài 1: Order Status

Tạo sealed interface OrderStatus với các states:

  • Draft (có draft version)
  • Submitted (có submission time)
  • Processing (có processor ID)
  • Completed (có completion time và result)
  • Failed (có failure reason)

Viết method getStatusMessage(OrderStatus status) sử dụng switch expression.

Bài 2: Expression Evaluator

Tạo sealed interface MathExpr với:

  • Constant(double value)
  • Add(MathExpr left, MathExpr right)
  • Multiply(MathExpr left, MathExpr right)
  • Negate(MathExpr expr)

Viết method double evaluate(MathExpr expr) để tính giá trị.

Bài 3: Event System

Tạo sealed interface UserEvent với:

  • UserRegistered(String userId, LocalDateTime timestamp)
  • UserLoggedIn(String userId, String ipAddress, LocalDateTime timestamp)
  • UserLoggedOut(String userId, LocalDateTime timestamp)
  • UserDeleted(String userId, String reason, LocalDateTime timestamp)

Viết method void handleEvent(UserEvent event) xử lý các events.

Tài liệu tham khảo

Đọc thêm