CodeGym /課程 /JAVA 25 SELF /Sealed classes:語法與應用

Sealed classes:語法與應用

JAVA 25 SELF
等級 65 , 課堂 0
開放

1. Sealed 類別語法:外觀與寫法

先從 Java OOP 的經典問題談起:開放的繼承階層。在一般 Java 裡,只要你的類別不是用 final 宣告,任何人都可以繼承它。這很方便,但有時會帶來意料之外的情況——例如你無法事先知道究竟有哪些類別會成為你的子類,因此也無法保證你在 switchif-else 中處理到了所有情形。

結果就是,當你按型別處理物件(例如在 switch 中做模式比對)時,只好加上一個 default 分支「以防萬一」:萬一哪裡突然多了個新子類呢?

Sealed 類別解決了這個問題:它允許你明確限制子類清單。這讓階層變得封閉可控,你的程式碼也更可預期、更安全。

基本語法

Sealed 類別在 Java 17 引入。宣告時使用修飾詞 sealed,允許的子類則用關鍵字 permits 指定:

public sealed class Shape permits Circle, Rectangle, Square {
    // 所有圖形的通用行為
}

這裡我們宣告了類別 Shape,只有 CircleRectangleSquare 能成為它的直接子類。其他任何類別都無法擴充 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 類別讓開發者能完全控制繼承階層。這對網域模型特別重要,因為某些選項集合必須是固定的(例如訂單狀態:NewPaidCancelled)。

簡化維護與演進

當你知道所有子類時,新增功能、維護與重構都更容易。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 明確列出所有子類。
  • 所有子類都必須是 finalsealednon-sealed 其中之一。
  • 子類必須宣告在同一個檔案,或至少能被編譯器看見。

抽象 sealed 類別

Sealed 類別可以是抽象類別、interface,也可以是一般類別。例如:

public sealed abstract class Expr permits Const, Add, Mul {}

與其他修飾詞的相容性

  • sealed 類別本身不能宣告為 finalnon-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 類別在哪裡特別有用?凡是你有固定選項集合的地方:

  • 操作結果:SuccessError(如上例)。
  • 訂單狀態:NewPaidCancelled
  • 解析器的抽象語法樹(AST)。
  • API 回應:OkNotFoundError
  • 系統事件:UserLoggedInUserLoggedOutUserRegistered

6. 使用 sealed 類別時的常見錯誤

錯誤 №1:在 permits 中遺漏了某些子類。
如果你沒有在 permits 中列出所有需要的子類,編譯器會立刻抱怨。比如你只寫了 permits Circle, Rectangle 卻漏了 Square(而程式中確實有這個類別)——就會出錯。

錯誤 №2:子類不是 finalsealed 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 之後才出現。如果你在更舊的版本中使用它,可能會得到編譯錯誤,或根本無法建置專案。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION