1. Sealed 類別語法:外觀與寫法
先從 Java OOP 的經典問題談起:開放的繼承階層。在一般 Java 裡,只要你的類別不是用 final 宣告,任何人都可以繼承它。這很方便,但有時會帶來意料之外的情況——例如你無法事先知道究竟有哪些類別會成為你的子類,因此也無法保證你在 switch 或 if-else 中處理到了所有情形。
結果就是,當你按型別處理物件(例如在 switch 中做模式比對)時,只好加上一個 default 分支「以防萬一」:萬一哪裡突然多了個新子類呢?
Sealed 類別解決了這個問題:它允許你明確限制子類清單。這讓階層變得封閉可控,你的程式碼也更可預期、更安全。
基本語法
Sealed 類別在 Java 17 引入。宣告時使用修飾詞 sealed,允許的子類則用關鍵字 permits 指定:
public sealed class Shape permits Circle, Rectangle, Square {
// 所有圖形的通用行為
}
這裡我們宣告了類別 Shape,只有 Circle、Rectangle 與 Square 能成為它的直接子類。其他任何類別都無法擴充 Shape——編譯器不會允許。
重要:列在 permits 中的所有類別,必須在同一個檔案中宣告,或至少要能被編譯器看見(通常在同一個套件中)。順帶一提,若所有子類都與 sealed 類別宣告在同一個檔案裡,甚至可以省略 permits——編譯器會自動理解。
範例:
// 全都在同一檔案中 - permits 可省略
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
對子類的要求
每個子類都必須明確指定自己的狀態:
- 標記為 final(禁止進一步繼承),
- 或標記為 sealed(並在下一層繼續限制繼承),
- 或標記為 non-sealed(允許自由繼承,移除限制)。
範例:
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 —— 終止於此,無法再被繼承。
- Rectangle —— 本身是 sealed,且只允許兩個子類。
- Square —— non-sealed,任何人都可以擴充。
最小範例
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
試著宣告一個新類別 public class Wolf extends Animal {} —— 你會得到編譯錯誤:
class Wolf is not allowed to extend sealed class Animal
2. Sealed 類別的應用:何時與為何使用
模式比對(Pattern matching)與 switch
Sealed 類別特別適合搭配 switch 的模式比對。當編譯器知道所有可能的子類時,它能驗證你是否處理了每一種情形,甚至不再要求提供 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("太棒了!");
switch (result) {
case Success s -> System.out.println("成功:" + s.data);
case Error e -> System.out.println("錯誤:" + e.message);
}
}
}
編譯器知道 Result 不會有其他變體,因此不需要 default。若你漏處理其中一個選項,編譯器會立刻提醒你。
請注意:自 Java 21 起,如果在 switch 中沒有覆蓋所有情形,你會得到編譯錯誤。在更早的版本(17–20)中可能需要 default,但 IDE 仍會提示覆蓋不完整。
安全與可控性
Sealed 類別讓開發者能完全控制繼承階層。這對網域模型特別重要,因為某些選項集合必須是固定的(例如訂單狀態:New、Paid、Cancelled)。
簡化維護與演進
當你知道所有子類時,新增功能、維護與重構都更容易。IDE 也會「知道」所有選項,能在自動完成與分析上提供更好的協助。
3. Sealed 類別的實用範例
範例 1:幾何圖形
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; }
}
現在可以安全地搭配模式比對使用 switch:
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("半徑為 " + c.radius + " 的圓");
case Rectangle r -> System.out.println("長方形 " + r.width + "×" + r.height);
case Square s -> System.out.println("邊長為 " + s.side + " 的正方形");
}
範例 2:金融交易
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;
}
}
在處理器中,你可以確信自己沒有漏掉任何交易型別:
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("存款:" + d.amount);
case Withdraw w -> System.out.println("提款:" + w.amount);
case Transfer t -> System.out.println("轉帳:" + t.amount + " 到 " + t.toAccount);
}
範例 3:含 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 {}
// 現在任何人都可以繼承 SmsNotification
public class ViberNotification extends SmsNotification {}
4. Sealed 類別的特性、限制與細節
修飾詞要求
- Sealed 類別必須透過 permits 明確列出所有子類。
- 所有子類都必須是 final、sealed 或 non-sealed 其中之一。
- 子類必須宣告在同一個檔案,或至少能被編譯器看見。
抽象 sealed 類別
Sealed 類別可以是抽象類別、interface,也可以是一般類別。例如:
public sealed abstract class Expr permits Const, Add, Mul {}
與其他修飾詞的相容性
- sealed 類別本身不能宣告為 final 或 non-sealed。
- interface 也可以是 sealed(非常實用!)。
搭配 record 類別
record 類別可以成為 sealed 類別的子類型,只要它們宣告為 final(預設 record 一定是 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. 實用細節
Sealed 類別與模式比對:有何關聯
Sealed 類別的最大特色就是可以做「完備」的模式比對。聽起來嚇人,其實很簡單。因為編譯器知道所有可能的變體,你就能放心地寫不含 default 分支的 switch:
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
如果你漏掉某個選項,編譯器就不會讓你建置專案——既方便又安全。
實務場景中的應用
Sealed 類別在哪裡特別有用?凡是你有固定選項集合的地方:
- 操作結果:Success、Error(如上例)。
- 訂單狀態:New、Paid、Cancelled。
- 解析器的抽象語法樹(AST)。
- API 回應:Ok、NotFound、Error。
- 系統事件:UserLoggedIn、UserLoggedOut、UserRegistered。
6. 使用 sealed 類別時的常見錯誤
錯誤 №1:在 permits 中遺漏了某些子類。
如果你沒有在 permits 中列出所有需要的子類,編譯器會立刻抱怨。比如你只寫了 permits Circle, Rectangle 卻漏了 Square(而程式中確實有這個類別)——就會出錯。
錯誤 №2:子類不是 final、sealed 或 non-sealed。
如果子類沒有使用正確的修飾詞,編譯器會報錯:「Class must be either final, sealed or non-sealed」。
錯誤 №3:子類宣告在另一個檔案中,且編譯器無法看見。
列在 permits 中的所有類別都必須能被編譯器存取——要嘛在同一檔案,要嘛在同一套件中。
錯誤 №4:在 switch 中沒有覆蓋所有選項。
如果你在含有模式比對的 switch 中使用 sealed 類別,卻漏掉某個變體,編譯器就不會讓你建置專案。這是好事——能避免遺漏任何情況。
錯誤 №5:嘗試從不在 permits 中的類別繼承 sealed 類別。
如果你嘗試建立一個未列於 permits 的子類,會出現錯誤:「is not allowed to extend sealed class」。
錯誤 №6:在舊版 JDK 中使用 sealed 類別。
Sealed 類別只在 Java 17 之後才出現。如果你在更舊的版本中使用它,可能會得到編譯錯誤,或根本無法建置專案。
GO TO FULL VERSION