Java 17 sealed クラス完全解説|sealed・non-sealed・final の違いと permits の使い方
Java 15 で sealed クラス / インターフェースがプレビュー機能として導入され、Java 17 からは正式機能になりました。sealed は中国語圏では「彌封」や「密封」と訳されることもありますが、本記事では英文のキーワード sealed をそのまま使います。sealed はクラスの継承やインターフェースの実装を制限するための仕組みです。Java の class のアクセスレベルは public・protected・(package-private)・private の 4 種類がありますが、Java 17 より前は継承の制御は「無印(誰でも継承できる)」か「final(誰も継承できない)」の 2 択しかなく、「特定のクラスにだけ継承を許可する」という中間の選択肢がありませんでした。この穴を埋めるために導入されたのが sealed です。
概念の説明
新しく追加されたキーワードは sealed・non-sealed・permits の 3 つです。sealed をクラスやインターフェースに付けて sealed と宣言し、その後ろに permits を続けて「継承/実装を許可する class・interface」を列挙します。
sealed なクラスやインターフェースを継承するサブクラス(以降サブクラスと呼びます)は、受け継いだ sealed 状態をどう扱うかを明示する必要があります。選択肢は 3 つ。1 つ目は、サブクラスにも sealed を付けて、自分の permits リストで引き続き継承先を制限する。2 つ目は non-sealed を付けて制限を解除し、どんなクラスからでも継承できる通常の class に戻す。3 つ目は以前からある final を付けて、どのクラスからも継承できない最終クラスにする。何も付けないとコンパイラが以下のメッセージで叱ってきます。
The class Penguin with a sealed direct superclass or a sealed direct superinterface Bird should be declared either final, sealed, or non-sealed
interface と class それぞれの挙動は、以下のようになります。
Sealed interface
sealed interface は permits キーワードで「継承を許可する interface」または「実装を許可する class」を列挙します。sealed interface を継承する interface は、自分が sealed か non-sealed かを宣言する必要があります。
継承先の interface に sealed interface を選べば、再び「自分の継承先を列挙する」状態に戻ります。non-sealed interface を選べば通常の interface に戻り、他の interface から継承されたり、class から実装されたりできます。
sealed interface を実装する class は、自分が sealed・non-sealed・final のいずれかを宣言する必要があります。それぞれの違いは次節で解説します。
Sealed class
sealed class は permits キーワードで「継承を許可する class」を列挙します。sealed class を継承する class は、自分が sealed・non-sealed・final のいずれかを宣言する必要があります。
sealed class を選べば、再び「自分の継承先を列挙する」状態に戻ります。non-sealed class を選べば通常の class に戻り、どんなクラスからでも継承されるようになります。final class を選べば、どのクラスからも継承できない最終クラスになります。
実装例
以下のクラスと interface には public・protected・private などのアクセス修飾子を付けていません。テストと閲覧の便宜のためで、実際のコードではもちろん指定できます。
Sealed interface: Animal(動物)
まず Animal インターフェースを作ります。sealed interface として宣言し、Chordata(脊索動物)のみ実装を許可します。
sealed interface Animal permits Chordata {
void eat();
}
補足:sealed interface の permits には class も interface も書けます。そして sealed interface を継承する interface は sealed か non-sealed のみが選べます。final interface は意味がないためです…… とはいえ Java 8 で interface にデフォルトメソッドが追加された後は、「継承も実装もできない interface」でも静的定数やデフォルト振る舞いの入れ物として使い道はあるかもしれません。
Sealed class: Chordata(脊索動物)
Chordata クラスを作り、Animal interface の eat() メソッドを実装します。そして Bird(鳥)のみが継承できるように設定します。
sealed class Chordata implements Animal permits Bird {
public void eat() {
System.out.printf("私は %s です。食事中です。%n",
getClass().getSimpleName());
}
}
Sealed class: Bird(鳥)
次に Bird クラス。Parrot(オウム)と Penguin(ペンギン)のみが継承できるように設定します。大抵の鳥は飛べるので、fly() メソッドを追加します。
sealed class Bird extends Chordata permits Parrot, Penguin {
public void fly() {
System.out.printf("私は %s です。飛んでいます。%n",
getClass().getSimpleName());
}
}
Non-sealed class: Parrot(オウム)
Parrot クラスを作り、non-sealed class として宣言します。これで通常の Java class に戻り、どんなクラスからでも継承できるようになります。すべての種のオウムが話せるわけではありませんが、歌うのはだいたい得意なので、sing() メソッドを追加します。
non-sealed class Parrot extends Bird {
public void sing() {
System.out.printf("私は %s です。歌っています。%n",
getClass().getSimpleName());
}
}
Normal class: GrayParrot(ヨウム)
GrayParrot クラスを作り、Parrot を継承します。Parrot がすでに non-sealed 化されているため、通常の Java class に戻っており、どのクラスからでも継承できます。ヨウム(アフリカのグレイパロット)はよく喋るオウムなので、talk() メソッドを追加します。
class GrayParrot extends Parrot {
public void talk() {
System.out.printf("私は %s です。話しています。%n",
getClass().getSimpleName());
}
}
Final class: Penguin(ペンギン)
Penguin クラスを作り、Bird(鳥)を継承します。final class を付けているので、Penguin はもうどのクラスからも継承できません。ペンギンは飛べないので 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());
}
}
使用例
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 です。泳いでいます。