Javaのシールクラス(sealed class)とは?permitsとfinal/sealed/non-sealed・switch網羅の使い方
シールクラスは、継承できるサブクラスを作者が名指しした型だけに限定するJavaの仕組みです。Java 17で正式機能になり、sealedとpermitsでクラス階層を閉じることで、想定外のサブクラスを封じるだけでなく、switchのパターンマッチと組み合わせて「取りうる型を全部処理したか」をコンパイラに検査させられます。この記事では宣言構文、許可サブクラスに必須の3修飾子、継承・インターフェースとの違い、そして使うべき場面と避けるべき場面までを実装者向けに整理します。
まとめ:シールクラスの要点
- 役割:
sealedを付けpermitsで継承を許可する型を列挙し、それ以外からの継承をコンパイルエラーにする。 - 許可サブクラスの必須修飾子:
permitsで挙げた各型はfinal・sealed・non-sealedのいずれかを必ず付ける(無指定はコンパイルエラー)。 - 最大の実用価値:Java 21で正式化した
switchのパターンマッチと組み合わせると、全サブクラスを網羅した時点でdefaultが不要になり、型を追加し忘れるとコンパイルが落ちる。 - 使いどころ:取りうる種類が有限で固定的なドメイン型(支払い方法、式の構文木、状態)。逆にライブラリの拡張点など第三者が継承する前提の型には向かない。
- 導入時期:JEP 360(JDK 15プレビュー)→ JEP 397(JDK 16)→ JEP 409でJDK 17に正式導入。
シールクラスとは:継承できる型をpermitsで閉じる仕組み
通常のJavaクラスは、finalを付けない限りどこからでも継承できます。シールクラスはこの前提を逆転させ、「継承してよいクラスはこれだけ」と作者が明示します。sealed修飾子を付けたクラスは、permits句に挙げた型からしか継承できず、それ以外がextendsしようとするとコンパイルエラーになります。
ねらいは、クラス階層を「閉じた集合」として扱えるようにすることです。取りうるサブタイプが有限だと保証できれば、後述のswitch網羅チェックのように、コンパイラがその集合を前提とした検査を行えます。継承を禁止するfinalと、無制限に開く通常クラスの中間に位置する、第3の選択肢だと捉えると理解しやすいです。
Java 17で正式化された経緯
シールクラスはJEP 360としてJDK 15にプレビュー導入され、JEP 397(JDK 16)で第2プレビューを経て、JEP 409によりJDK 17で正式機能になりました。したがってプレビューフラグなしで使えるのはJDK 17以降です。non-sealedはJava史上初のハイフン付きキーワードとして、このとき追加されました。
sealedとpermitsの書き方:宣言構文と3つのサブクラス修飾子
シールクラスの宣言はsealedとpermitsの2つで構成します。次のVehicleはCarとTruckにだけ継承を許可します。
public abstract sealed class Vehicle permits Car, Truck { }
public final class Car extends Vehicle { }
public non-sealed class Truck extends Vehicle { }
ここでclass Motorcycle extends Vehicle { }を書くと、Motorcycleはpermitsに無いためコンパイルエラーになります。一方Truckはnon-sealedなので、class BigTruck extends Truck { }のように第三者が継承を続けられます。インターフェースにもsealedを付けられ、その場合permitsには実装クラスや子インターフェースを列挙します。
許可サブクラスの3修飾子(final・sealed・non-sealed)
ここが最も誤解されやすい点です。permitsに挙げた各サブクラスは、階層をどう続けるかを次の3修飾子のいずれかで必ず宣言しなければなりません(付け忘れはコンパイルエラー)。
| 修飾子 | 意味 | さらに継承 |
|---|---|---|
final |
ここで階層を打ち止め | 不可 |
sealed |
さらに限定した相手にだけ継承を許可 | permitsで指定した型のみ可 |
non-sealed |
この枝だけ通常クラスに戻し開放 | 誰でも可 |
「サブクラスはfinalだけ」と説明する解説が散見されますが誤りです。non-sealedを選べば、その型から先は再び自由に継承でき、シールの制約を一部だけ解くといった設計ができます。なおrecordは暗黙にfinalのため、permits先にrecordを並べる場合は修飾子を書かなくてもfinal扱いになります。
permitsを省略できる条件と配置制約
permits句は常に必須ではありません。許可したいサブクラスをすべて同じソースファイル内に書く場合は、コンパイラが自動で許可対象を判定するためpermitsを省略できます。
配置には制約があります。許可サブクラスは、シールクラスと同じモジュール(名前付きモジュールの場合)または同じパッケージ(無名モジュールの場合)に置かなければなりません。別ファイルに分けるのは可能ですが、モジュール/パッケージ境界を越えて許可サブクラスを散らすことはできません。
継承・インターフェースとの違い
通常のクラス継承との違い
通常のクラスは「開いた」拡張点で、作者が把握しないサブクラスが後から増えても成立するように設計します。シールクラスは逆に「閉じた」型集合を宣言し、増えるサブクラスを作者がコントロールします。ライブラリの利用者に自由な拡張を促したいなら通常のクラスや継承の基本と使いどころの考え方が適し、種類が有限で網羅したいならシールクラスが適します。
sealed interfaceとの使い分け
制約したい対象が「共通のデータと実装を持つ具象階層」ならシールドなclass、「共通の振る舞いだけを定義し実装は各型に任せる」ならシールドなinterfaceを選びます。実務では、sealed interfaceと各実装recordを組み合わせて「取りうる値の有限集合」を表現するのが定番です。インターフェースは多重実装ができるため、複数の型階層に同時に属させたいときはインターフェース側にシールを掛けます。
switch網羅チェックとの組み合わせ(sealed最大の使いどころ)
シールクラスが単なる「継承禁止の上位版」で終わらないのは、パターンマッチングと組むと威力を発揮するからです。Java 21で正式化したswitchのパターンマッチ(JEP 441)は、対象がシールドな型のときpermitsの一覧を見て網羅性を判定します。すべての許可サブクラスをcaseで処理していれば、defaultを書かなくてもコンパイルが通ります。取りうる形を有限に閉じるため、ここではsealed interfaceとrecordを組み合わせます。
public sealed interface Shape permits Circle, Square { }
public record Circle(double radius) implements Shape { }
public record Square(double side) implements Shape { }
recordはradius()やside()といったアクセサを自動生成し、暗黙にfinalです。Shapeはインターフェースなので直接インスタンス化されません。この2点により取りうる型がCircleとSquareの2つに閉じ、次のswitchはdefaultなしで網羅と判定されます。
public double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
};
}
この設計の利点は、あとでTriangleをpermitsに足すと、上のswitchがコンパイルエラーになる点です。処理漏れが実行時ではなくビルド時に発覚するため、型の追加に強い分岐が書けます。sealed interfaceとrecordを合わせたこの形はいわゆる代数的データ型(取りうる形が有限に閉じたデータ)そのもので、木構造や状態機械の実装が簡潔になります。recordの詳しい記法はJavaのレコードクラスの使い方を参照してください。
ひとつ注意点があります。許可サブクラスのどれかをnon-sealedにすると、その枝は未知のサブクラスが無限に増えうるため網羅を保証できず、コンパイラはswitchにdefaultを要求します。網羅チェックの恩恵を受けたい階層ではnon-sealedを避けるのが原則です。
シールクラスを使うべき場面・避けるべき場面
シールクラスは「種類が有限で、その全種類を1か所で漏れなく扱いたい」ときに最も効きます。具体的には、支払い方法(クレジット/銀行振込/ポイント)、式や命令を表す構文木のノード、注文の状態遷移など、取りうる値が設計上固定されているドメイン型です。これらはswitch網羅チェックと相性がよく、種類が増えたときの修正漏れをコンパイラが検出します。
逆に、避けるべき場面もはっきりしています。フレームワークやライブラリの拡張点として第三者に自由な継承を提供したい型には向きません。許可リストを作者が握るため、利用者は新しいサブタイプを追加できないからです。また、サブタイプが頻繁に増減する流動的な段階の設計に早く導入すると、permitsとモジュール配置の制約が変更コストになります。まず種類が安定してからシールドに閉じるのが安全です。単に「他人に継承させたくない」だけなら、許可リストの管理が不要なfinalで十分で、シールクラスは過剰です。
よくある質問
sealed classはどのバージョンから使えますか?
プレビューを含めればJDK 15(JEP 360)から、正式機能としてはJDK 17(JEP 409)からです。JDK 17以降であればプレビューフラグなしで利用できます。
permitsは必ず書く必要がありますか?
許可するサブクラスをすべて同じソースファイル内に宣言する場合は省略できます。別ファイルに分けるときはpermitsで明示的に列挙します。
non-sealedとは何ですか?
許可サブクラスに付ける修飾子の1つで、その型から先の継承を再び開放します。シールドな階層の一部の枝だけを通常クラスに戻したいときに使いますが、その枝ではswitchの網羅チェックが効かなくなります。
C#のsealedとJavaのsealedは同じですか?
名前は同じでも意味が異なります。C#のsealedはそのクラスの継承を完全に禁止する(Javaのfinalに相当)機能です。一方Javaのsealedは継承自体は許可しつつ、継承できる相手をpermitsで限定します。C#で「継承先を限定」する仕組みには直接対応しません。
シールクラスとfinalはどう使い分けますか?
継承を一切させたくないならfinal、継承は許可するが相手を有限集合に固定して網羅的に扱いたいならsealedを使います。許可先が1つも無いならfinalで十分です。