在Java 15的時候出現了sealed class / interface的預覽功能,從Java 17開始sealed成為正式的新功能。有些人將sealed翻譯為彌封類別,或是密封類別,本文中以原文sealed來稱呼。sealed是用來限制類別繼承與介面實作用的。一個Java Class的可用性有四種狀態,分別是public、protected、(無標示)、private,但可否繼承的設定卻只有兩種,分別是無標示與final兩個等級。要不就是都可以繼承,要不就是都不能繼承,沒辦法選擇只有指定Class可以繼承,這就是sealed出現來要解決的問題。

概念說明

本次新增多了幾個關鍵字,分別是sealednon-sealedpermits。sealed用來標注一個類別或介面成為sealed,被標為sealed的類別或介面一定要在後面加上permits關鍵字來指定哪些類別或介面可以實作或繼承。

繼承Sealed Class或實作Sealed Interface的子類別/子介面(以下只稱呼子類別)必須標註要如何處理原本的Sealed狀態。有三種選擇,第一個是在子類別的前面加上sealed,讓子類別繼續限制繼續限制只能被permits指定的類別繼承。第二種可以加上non-sealed代表變回普通的類別,可以被任何類別繼承。最後是加上原本就有的final關鍵字,跟之前一樣代表不能被任何類別繼承的最終類別。不能什麼都不寫,會跳出通知說:

The class Penguin with a sealed direct superclass or a sealed direct superinterface Bird should be declared either final, sealed, or non-sealed

下方重新整理一下。

Sealed Interface

Sealed Interface要透過permits關鍵字指定哪些Interface可以繼承自己,或是哪些Class可以實作自己。繼承Sealed Interface要指定自己是Sealed Interface還是Non-sealed Interface。

繼承的Interface選擇sealed interface又會跟回到上面的狀態,要重新指定可以繼承或實作自己的人選;選擇non-sealed interface就會變回普通的Interface,可以被其他Interface繼承或是被Class實作。

實作Sealed Interface的Class要指定自己是Sealed Class、Non-sealed Class、Final Class,差別可以看下一小節說明。

Sealed Class

Sealed Class要透過permits關鍵字指定哪些Class可以繼承自己,繼承Sealed Class的Class要指定自己是Sealed Class、Non-sealed Class、Final Class。

選擇sealed class的狀況就要重複上面說明的那樣;選擇non-sealed class後就會變回普通的Class,可以被其他Class繼承;選擇final class代表不能被任何Class繼承。


範例說明

以下類別與介面都沒加上publicprotectedprivate等修飾關鍵字,只是為了方便測試與觀看,實際使用時依然能夠指定這些關鍵字。

Sealed Interface 動物

首先我們建立一個Animal(動物)介面,是一個sealed interface,並且設定只有Chordata(脊索動物)類別可以實作此介面。

/**
 * klab.tw
 */
sealed interface Animal permits Chordata {
    void eat();
}

補充一下,sealed interface的permits可以寫class也可以寫interface,而一個interface繼承一個sealed interface的時候只能選擇是sealed還是non-sealed,畢竟final interface是沒有意義的… 雖然Java 8給interface新增default method之後,一個不能被繼承或實作的interface的似乎還是有用啦。

Sealed Class 脊索動物

建立一個Chordata類別,實作了Animal介面的eat()方法,然後設定只有Bird(鳥)類別可以繼承Animal。

sealed class Chordata implements Animal permits Bird {
    
    public void eat() {
        System.out.printf("我是%s,我在吃飯。%n", 
            getClass().getSimpleName());
    }
}

Sealed Class 鳥

再來是Bird類別,我們設定只有Parrot(鸚鵡)與Penguin(企鵝)可以繼承Bird。大部分鳥類會飛,我們添增一個fly()方法。

sealed class Bird extends Chordata permits Parrot, Penguin {

    public void fly() {
        System.out.printf("我是%s,我在飛翔。%n", 
            getClass().getSimpleName());
    }
}

Non-Sealed Class 鸚鵡

我們建立Parrot(鸚鵡)類別,並且標註為non-sealed class,代表變成普通的Java Class可以被任何類別繼承。不是所有種類的鸚鵡都會講話,但通常都滿會唱歌,我們新增一個sign()方法。

non-sealed class Parrot extends Bird {

    public void sing() {
        System.out.printf("我是%s,我在唱歌。%n", 
            getClass().getSimpleName());
    }
}

Nomarl Class 灰鸚鵡

建立GrayParrot(灰鸚鵡)類別繼承Parrot。因為剛才的Parrot已經被標註為non-sealed了,變回普通的Java Class,因此任何類別都可以繼承Parrot。灰鸚鵡是一種很會講話的鸚鵡,因此增加一個talk()方法。

class GrayParrot extends Parrot {

    public void talk() {
        System.out.printf("我是%s,我在說話。%n", 
            getClass().getSimpleName());
    }
}

Final Class 企鵝

現在建立一個Penguin(企鵝)類別,繼承了Bird(鳥)類別,使用了final class修飾代表Penguin不能再被任何類別繼承了。因為企鵝不會飛,因此Override(覆寫)了fly()方法,改成不會飛。然後再為會游泳的企鵝加上swim()方法。

final class Penguin extends Bird {

    @Override
    public void fly() {
        System.out.printf("我是%s,我不會飛。%n", 
            getClass().getSimpleName());
    }
    public void swim() {
        System.out.printf("我是%s,我在游泳。%n", 
            getClass().getSimpleName());
    }
}

使用範例

// https://klab.tw/2023/01/java-17-sealed-non-sealed-and-final-class/
var chor = new Chordata();
chor.eat();  // 我是Chordata,我在吃飯。

var bird = new Bird();
bird.eat();  // 我是Bird,我在吃飯。
bird.fly();  // 我是Bird,我在飛翔。

var part = new Parrot();
part.eat();  // 我是Parrot,我在吃飯。
part.fly();  // 我是Parrot,我在飛翔。
part.sing(); // 我是Parrot,我在唱歌。

var gray = new GrayParrot();
gray.eat();  // 我是GrayParrot,我在吃飯。
gray.fly();  // 我是GrayParrot,我在飛翔。
gray.sing(); // 我是GrayParrot,我在唱歌。
gray.talk(); // 我是GrayParrot,我在說話。

var peng = new Penguin();
peng.eat();  // 我是Penguin,我在吃飯。
peng.fly();  // 我是Penguin,我不會飛。
peng.swim(); // 我是Penguin,我在游泳。