Java : シールクラスの基本(Sealed Class)
シールクラスを使うと、許可したクラスだけが継承できるようにクラス拡張を制限できます。
 Java 17 の言語仕様として新しく追加されました。
本記事では、シールクラスの基本的な使い方を解説していきます。
シールクラスとは
3 シール・クラス
シール・クラスおよびインタフェースは、それらを拡張または実装できる他のクラスまたはインタフェースを制限します。
上記の記事は、公式によるシールクラスの紹介記事です。
 そちらも一読することをおすすめします。
シールクラスは、許可したクラスのみが継承できるように制限されたクラスです。
 シールインタフェースについても同様で、実装先のクラスを制限できます。
キーワードは sealed になります。
 "シールド" と読みたくなりますが、公式の翻訳に合わせて本記事では "シール" とします。
Java 17 より前のバージョンでは、継承を制限するには、
- いくらでも継承可能 (デフォルト)
 - 継承不可 (final)
 
の2つの方法がありました。
 (アクセス修飾子による制限については今回は除きます)
まずデフォルトの例を見てみましょう。
class Base {
}
class SubA extends Base {
}
class SubB extends Base {
}
class SubC extends Base {
}
 
      Baseクラスは特に制限なく、いくらでも継承可能です。
次に final の例です。
final class Base {
}
// コンパイルエラー
class SubA extends Base {
}
// コンパイルエラー
class SubB extends Base {
}
// コンパイルエラー
class SubC extends Base {
}
 
      final class Base
^^^^^
 
      class 宣言に final をつけると、Baseクラスは一切継承できないように制限されます。
 継承しようとするとコンパイルエラーとなります。
そして シールクラス です。
シールクラスは、デフォルトと final の中間のようなイメージです。
 許可したクラスのみ継承可能となります。
sealed class Base permits SubA, SubB {
}
final class SubA extends Base {
}
final class SubB extends Base {
}
// コンパイルエラー
final class SubC extends Base {
}
 
      sealed class Base permits SubA, SubB 
^^^^^^            ^^^^^^^
 
      sealed キーワードを使い、Baseクラスがシールクラスであることを宣言します。
そして、permits キーワードで、継承を許可するクラスを指定します。
 上記例では SubA, SubB が許可されています。
許可されていないクラスで Base を継承しようとするとコンパイルエラーとなります。
 上記例では SubC が許可されていません。
また、許可されたクラス側では、シールクラスを継承するさいに、以下のいずれかのキーワードが必要となります。
- final
 - sealed
 - non-sealed
 
これらのキーワードをつけないとコンパイルエラーとなります。
 上記例では final キーワードをつけています。
final class SubA extends Base
^^^^^
final class SubB extends Base
^^^^^
 
      各キーワードの詳細については後述します。
サブクラス側の制約
シールクラスを継承する側(サブクラス)にはいくつかの制約があります。
 1つずつ見ていきましょう。
配置
継承する側のクラスは、ベースとなるシールクラスと 同一モジュール に配置されている必要があります。
 もしくは、モジュール未使用 (厳密には名前のないモジュール) の場合は、同一パッケージ に配置されている必要があります。
例えば、シールクラスとそのサブクラスがそれぞれが別モジュールに配置されている場合、コンパイルエラーとなります。
シールの振る舞いを明示
シールクラスを継承したクラスは、自身のシールの振る舞いを明示する必要があります。
 具体的には、以下のいずれかを選択する必要があります。
- final
 - sealed
 - non-sealed
 
キーワードを省略した場合はコンパイルエラーとなります。
sealed class Base permits SubA, SubB {
}
// コンパイルOK (キーワード final)
final class SubA extends Base {
}
// コンパイルエラー (キーワード省略)
class SubB extends Base {
}
 
      final
シールクラスのサブクラスに final をつけると、それ以降の継承はできなくなります。
 通常クラスに final をつけたときと同じですね。
sealed class Base permits Sub {
}
final class Sub extends Base {
}
// Sub の継承はできません。
// コンパイルエラー
final class SubSubA extends Sub {
}
// コンパイルエラー
final class SubSubB extends Sub {
}
 
      sealed
シールクラスのサブクラスに sealed をつけると、そのサブクラスもシールクラスとなります。
sealed class Base permits Sub {
}
sealed class Sub extends Base permits SubSubA, SubSubB {
}
final class SubSubA extends Sub {
}
final class SubSubB extends Sub {
}
// コンパイルエラー
final class SubSubC extends Sub {
}
 
      non-sealed
シールクラスのサブクラスに non-sealed をつけると、そのサブクラスは無制限に継承可能となります。
 通常クラスのデフォルトの制限と同じです。
sealed class Base permits Sub {
}
non-sealed class Sub extends Base {
}
final class SubSubA extends Sub {
}
final class SubSubB extends Sub {
}
final class SubSubC extends Sub {
}
 
      使いどころ
継承先を厳密に管理したい
例えば、不変オブジェクト となるクラスを作りたいとしましょう。
通常、不変オブジェクトのクラス宣言には final を用いて継承を禁止します。
 final をつけないと、意図せず可変なサブクラスを作られてしまう危険性があるためです。
// 不変クラスの例
public final class ImmutableA {
    private final int num;
    private final String str;
    public ImmutableA(int num, String str) {
        this.num = num;
        this.str = str;
    }
    public int getNum() {
        return num;
    }
    public String getStr() {
        return str;
    }
}
 
      しかし、あるクラスをベースとして、それを拡張して複数の不変クラスを作りたい、という場合もあるかもしれません。
 ベースクラスに final をつけると、サブクラスが作れないので困りますね。
そんなときには、シールクラスが有効でしょう。
継承できるのは許可したクラスだけです。
 それらのクラスを不変にすれば、ベースクラスも含めてすべてのクラスで不変性が保たれます。
網羅性
シールクラスは、許可したクラスのみ継承できます。
 この特徴は、別の言い方をすると、
- シールクラスのサブクラスを列挙できる
 
ということでもあります。
つまり、列挙型(Enum) と同じように網羅性があります。
Enum は拡張可能ですが、その列挙値のインスタンスは1つという制限があります。
 シールクラスを使えば「網羅性がありつつ、かつ、複数のインスタンスを生成」ということも可能です。
Enum の網羅性の判定には switch式 が便利です。
 そして、Java 21 ではシールクラスについても switch式 が使えるようになりました。
【コード例】
sealed interface Base permits SubA, SubB, SubC {
}
final class SubA implements Base {
    void a() {
        System.out.println("SubA!");
    }
}
final class SubB implements Base {
    void b() {
        System.out.println("SubB!");
    }
}
final class SubC implements Base {
    void c() {
        System.out.println("SubC!");
    }
}
 
      void test(Base base) {
    switch (base) {
        case SubA sub -> sub.a();
        case SubB sub -> sub.b();
        case SubC sub -> sub.c();
    }
}
 
      test(new SubA());
// 結果
// ↓
//SubA!
test(new SubB());
// 結果
// ↓
//SubB!
test(new SubC());
// 結果
// ↓
//SubC!
 
      シールクラスの網羅性については、オラクル公式のブログ記事にもあるので、そちらもご参照ください。
まとめ
シールクラスを使うと、許可したクラスだけが継承できるように制限できます。
- クラスの継承先(サブクラス)を厳密に管理したい
 - 列挙型のように、クラスに網羅性を持たせたい
 
上記のようなときに、シールクラスは有用なのかな、と思います。
関連記事
- 不変オブジェクト(イミュータブル) とは
 - if文の基本
 - while文の基本
 - for文の基本
 - プリミティブ型 (基本データ型)
 - リテラルの表記方法いろいろ
 - クラスの必要最低限を学ぶ
 - インタフェースの default メソッドとは
 - インタフェースの static メソッドの使いどころ
 - var (型推論) のガイドライン
 - アクセス修飾子の基本
 - 配列 (Array) の使い方
 - 拡張for文 (for-eachループ文)
 - switch文ではなくswitch式を使おう
 - try-with-resources文でリソースを自動的に解放
 - テキストブロックの基本
 - 列挙型 (enum) の基本
 - ラムダ式の基本
 - レコードクラスの基本 (Record Class)
 - メソッド参照の基本
 - 無名変数の使い方
 



