1. Cú pháp của sealed class: trông như thế nào
Hãy bắt đầu với một vấn đề kinh điển của OOP trong Java: hệ phân cấp thừa kế mở. Trong Java thông thường, bất kỳ ai cũng có thể kế thừa từ lớp của bạn, trừ khi nó được khai báo là final. Điều này tiện lợi, nhưng đôi khi dẫn tới những bất ngờ — ví dụ, bạn không thể biết trước chính xác những lớp nào sẽ là lớp con của lớp bạn, đồng nghĩa với việc không thể đảm bảo đã xử lý mọi trường hợp trong switch hoặc if-else.
Kết quả là, khi bạn viết xử lý đối tượng theo kiểu (ví dụ, qua pattern matching trong switch), bạn buộc phải thêm nhánh default “phòng hờ”: biết đâu ai đó ở đâu đó tạo ra một lớp con mới?
Sealed classes giải quyết vấn đề này: chúng cho phép giới hạn rõ ràng danh sách lớp kế thừa. Điều này khiến hệ phân cấp trở nên đóng và có kiểm soát, còn mã của bạn — dự đoán được và an toàn hơn.
Cú pháp cơ bản
Sealed classes xuất hiện trong Java 17. Chúng được khai báo bằng bổ từ sealed, và danh sách lớp kế thừa được phép được chỉ định bằng từ khóa permits:
public sealed class Shape permits Circle, Rectangle, Square {
// Hành vi chung cho mọi hình
}
Ở đây chúng ta khai báo lớp Shape, và chỉ các lớp Circle, Rectangle và Square mới có thể là lớp kế thừa trực tiếp của nó. Không ai khác có thể mở rộng Shape — trình biên dịch sẽ không cho phép.
Quan trọng: Tất cả các lớp được liệt kê trong permits phải được khai báo trong cùng một tệp hoặc phải hiển thị với trình biên dịch (thông thường là trong cùng package). Nhân tiện, nếu tất cả lớp kế thừa được khai báo trong cùng tệp với sealed class, có thể bỏ qua permits — trình biên dịch sẽ tự hiểu.
Ví dụ:
// Tất cả trong cùng một tệp - permits là không bắt buộc
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
Yêu cầu đối với lớp kế thừa
Mỗi lớp kế thừa bắt buộc phải chỉ rõ trạng thái của mình:
- là final (cấm kế thừa tiếp),
- hoặc là sealed (và tiếp tục giới hạn kế thừa),
- hoặc là non-sealed (cho phép kế thừa, gỡ bỏ giới hạn).
Ví dụ:
public sealed class Shape permits Circle, Rectangle, Square {}
public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
- Circle — là cuối cùng; không thể kế thừa tiếp.
- Rectangle — bản thân là sealed, chỉ cho phép hai lớp con.
- Square — non-sealed, ai cũng có thể mở rộng.
Ví dụ tối giản
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
Hãy thử khai báo lớp public class Wolf extends Animal {} — bạn sẽ nhận lỗi biên dịch:
class Wolf is not allowed to extend sealed class Animal
2. Ứng dụng của sealed class: dùng ở đâu và để làm gì
Pattern matching và switch
Sealed classes đặc biệt phù hợp với pattern matching trong switch. Nếu trình biên dịch biết mọi lớp con có thể có, nó có thể đảm bảo rằng bạn đã xử lý từng trường hợp và thậm chí không yêu cầu nhánh default.
public sealed interface Result permits Success, Error {}
public final class Success implements Result {
public final String data;
public Success(String data) { this.data = data; }
}
public final class Error implements Result {
public final String message;
public Error(String message) { this.message = message; }
}
public class Main {
public static void main(String[] args) {
Result result = new Success("Tuyệt!");
switch (result) {
case Success s -> System.out.println("Thành công: " + s.data);
case Error e -> System.out.println("Lỗi: " + e.message);
}
}
}
Trình biên dịch biết rằng Result không thể có biến thể nào khác và không yêu cầu default. Nếu bạn quên xử lý một trường hợp, trình biên dịch sẽ nhắc bạn ngay.
Lưu ý: bắt đầu từ Java 21, nếu trong switch không xử lý đầy đủ mọi trường hợp, bạn sẽ nhận lỗi biên dịch. Ở các phiên bản trước (17–20) có thể cần default, nhưng IDE vẫn sẽ cảnh báo về phạm vi bao phủ chưa đầy đủ.
An toàn và kiểm soát
Sealed classes cho phép lập trình viên kiểm soát hoàn toàn hệ phân cấp. Điều này đặc biệt quan trọng cho các mô hình miền, nơi tập hợp các biến thể phải cố định (ví dụ, trạng thái đơn hàng: New, Paid, Cancelled).
Đơn giản hóa bảo trì và phát triển mã
Khi bạn biết mọi biến thể lớp con, việc bổ sung tính năng mới, bảo trì mã và refactor trở nên dễ dàng hơn. IDE cũng sẽ “nắm rõ” mọi biến thể và có thể hỗ trợ tự động hoàn thành cũng như phân tích.
3. Ví dụ thực tế khi dùng sealed class
Ví dụ 1: Hình học
public sealed interface Shape permits Circle, Rectangle, Square {}
public final class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
}
public final class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
public final class Square implements Shape {
public final double side;
public Square(double side) { this.side = side; }
}
Bây giờ bạn có thể dùng switch với pattern matching một cách an toàn:
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("Hình tròn có bán kính " + c.radius);
case Rectangle r -> System.out.println("Hình chữ nhật " + r.width + "x" + r.height);
case Square s -> System.out.println("Hình vuông có cạnh " + s.side);
}
Ví dụ 2: Giao dịch tài chính
public sealed interface Transaction permits Deposit, Withdraw, Transfer {}
public final class Deposit implements Transaction {
public final double amount;
public Deposit(double amount) { this.amount = amount; }
}
public final class Withdraw implements Transaction {
public final double amount;
public Withdraw(double amount) { this.amount = amount; }
}
public final class Transfer implements Transaction {
public final double amount;
public final String toAccount;
public Transfer(double amount, String toAccount) {
this.amount = amount;
this.toAccount = toAccount;
}
}
Bây giờ trong bộ xử lý, bạn có thể chắc chắn rằng mình không bỏ sót loại giao dịch nào:
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("Nạp tiền: " + d.amount);
case Withdraw w -> System.out.println("Rút tiền: " + w.amount);
case Transfer t -> System.out.println("Chuyển khoản: " + t.amount + " tới " + t.toAccount);
}
Ví dụ 3: Hệ phân cấp có giới hạn với non-sealed
public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}
public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}
// Bây giờ bất kỳ ai cũng có thể kế thừa SmsNotification
public class ViberNotification extends SmsNotification {}
4. Đặc điểm, giới hạn và lưu ý của sealed class
Yêu cầu về bộ bổ từ
- Sealed class phải liệt kê rõ tất cả lớp kế thừa qua permits.
- Mọi lớp kế thừa phải là final, hoặc sealed, hoặc non-sealed.
- Các lớp kế thừa phải được khai báo trong cùng tệp hoặc hiển thị với trình biên dịch.
Sealed class trừu tượng
Sealed class có thể là abstract, là interface, hoặc là lớp thông thường. Ví dụ:
public sealed abstract class Expr permits Const, Add, Mul {}
Tương thích với các bổ từ khác
- Không thể khai báo sealed class là final hoặc non-sealed.
- interface cũng có thể là sealed (và điều này rất tiện!).
Sử dụng với record
Các record có thể là lớp con của sealed class nếu chúng được khai báo là final (mặc định record luôn là final):
public sealed interface Expr permits Const, Add, Mul {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
5. Những lưu ý hữu ích
Sealed class và pattern matching: liên hệ thế nào
Điểm “đắt” của sealed class là pattern matching mang tính bao quát. Nghe có vẻ ghê gớm nhưng hoạt động rất đơn giản. Trình biên dịch biết mọi biến thể có thể có, và bạn có thể thoải mái viết switch không cần nhánh default:
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
Nếu bạn không xử lý một biến thể nào đó, trình biên dịch sẽ không cho build dự án — điều này rất tiện và an toàn.
Ứng dụng trong bài toán thực tế
Sealed class thực sự hữu ích ở đâu? Ở mọi nơi bạn có một tập hợp biến thể cố định:
- Kết quả thao tác: Success, Error (như ví dụ ở trên).
- Trạng thái đơn hàng: New, Paid, Cancelled.
- Cây cú pháp trừu tượng (AST) cho trình phân tích cú pháp.
- Phản hồi API: Ok, NotFound, Error.
- Sự kiện trong hệ thống: UserLoggedIn, UserLoggedOut, UserRegistered.
6. Lỗi thường gặp khi làm việc với sealed class
Lỗi số 1: quên liệt kê tất cả lớp kế thừa trong permits.
Nếu bạn không liệt kê đầy đủ các lớp kế thừa qua permits, trình biên dịch sẽ lập tức báo lỗi. Ví dụ, nếu bạn viết permits Circle, Rectangle nhưng quên Square trong khi lớp này có tồn tại — bạn sẽ nhận lỗi.
Lỗi số 2: lớp kế thừa không final, sealed hoặc non-sealed.
Nếu lớp kế thừa không được khai báo với bổ từ cần thiết, trình biên dịch sẽ báo: “Class must be either final, sealed or non-sealed”.
Lỗi số 3: lớp kế thừa được khai báo ở tệp khác và không hiển thị với trình biên dịch.
Tất cả các lớp được liệt kê trong permits phải khả dụng với trình biên dịch — hoặc trong cùng tệp, hoặc trong cùng package.
Lỗi số 4: không xử lý đầy đủ mọi trường hợp trong switch.
Nếu bạn dùng sealed class trong switch với pattern matching và quên xử lý một trường hợp nào đó, trình biên dịch sẽ không cho build dự án. Điều này là tốt — bạn sẽ không bỏ sót bất kỳ trường hợp nào.
Lỗi số 5: cố gắng kế thừa từ sealed class nhưng không có trong permits.
Nếu bạn cố tạo một lớp kế thừa không được nêu trong permits, bạn sẽ nhận lỗi: “is not allowed to extend sealed class”.
Lỗi số 6: cố dùng sealed classes trên các phiên bản JDK cũ.
Sealed classes chỉ xuất hiện từ Java 17. Nếu bạn cố dùng chúng ở phiên bản cũ hơn, bạn sẽ nhận lỗi biên dịch hoặc không thể build dự án.
GO TO FULL VERSION