Java | Java 詳細・モダン文法:言語仕様詳細 – sealed class

Java Java
スポンサーリンク

sealed class を一言でいうと

sealed クラスは
「このクラスを継承してよい“サブクラスの種類”を、あらかじめ限定する仕組み」
です。

継承を完全に禁止する final と違って、
「継承は許すけど、誰でも自由に継承していいわけじゃない。許可したクラスだけね」
と宣言できるのが sealed です。

これによって、「この型には取りうるパターンがこれだけある」とコンパイラに教えられるようになり、
switch 式などで 網羅性チェック が効くようになります。


sealed class の基本構文とキーワードの関係

permits で「継承を許可するクラス」を列挙する

一番シンプルな例から見てみましょう。

public sealed class Shape
        permits Circle, Rectangle {
}

public final class Circle extends Shape {
    // ...
}

public final class Rectangle extends Shape {
    // ...
}
Java

ここでのポイントは次の通りです。

  • Shapesealed
  • permits Circle, Rectangle で「継承を許可するサブクラス」を明示
  • 実際に継承する側(Circle / Rectangle)は、final などの修飾が必須

sealed 階層に属するサブクラスは、必ず次のいずれかでなければなりません。

  • final(それ以上継承させない)
  • sealed(さらに限定付きで継承を許す)
  • non-sealed(そこから先は自由に継承してよい)

この 3 つのどれかを必ず付ける、というルールになっています。


sealed class が解決したい「継承のゆるさ」の問題

従来の継承は「誰でも継承できてしまう」

普通のクラスは、final でない限り、どこからでも継承できます。

public class Shape {
}

public class Triangle extends Shape {
}

public class StrangeShape extends Shape {
}
Java

Shape を設計した人が「Circle と Rectangle だけを想定していた」としても、
別の人が勝手に StrangeShape を作れてしまいます。

その結果、

  • 「Shape のサブクラスはこの 2 つだけ」と思い込んで書いた switch が破綻する
  • ライブラリの利用者が、想定外のサブクラスを作ってしまう

といった問題が起きます。

sealed class で「世界を閉じる」

sealed を使うと、「この継承階層の世界」を閉じることができます。

public sealed class Shape
        permits Circle, Rectangle {
}
Java

こう書いた瞬間、Shape を継承できるのは CircleRectangle だけになります。
他のクラスが extends Shape と書こうとすると、コンパイルエラーです。

「この抽象型の具体的なバリエーションは、これとこれとこれだけ」
と、型レベルで宣言できるようになるわけです。


sealed class と switch 式の相性(網羅性チェック)

「全部のサブタイプを扱っているか?」をコンパイラが見てくれる

先ほどの Shape を使って、switch 式を書いてみます。

String describe(Shape shape) {
    return switch (shape) {
        case Circle c -> "円です";
        case Rectangle r -> "四角です";
    };
}
Java

ここで default を書いていないのに、コンパイルは通ります。
なぜかというと、Shape のサブタイプは CircleRectangle だけだとコンパイラが知っているからです。

もし、Triangle というサブクラスを sealed 階層に追加したらどうなるか。

public sealed class Shape
        permits Circle, Rectangle, Triangle {
}
Java

この状態で先ほどの switch をコンパイルすると、
Triangle を扱っていないよ」とコンパイルエラーになります。

これが 網羅性チェック です。
「将来サブクラスが増えたときに、対応漏れをコンパイラが教えてくれる」
というのは、設計としてかなり強力です。

default に逃げなくてよくなる

従来は、サブクラスが増えるかもしれない不安から、
switchdefault を書きがちでした。

switch (shape) {
    case Circle c -> ...
    case Rectangle r -> ...
    default -> ...
}
Java

でも default に逃げると、
「本当は想定していないサブクラス」が来ても気づけません。

sealed class と網羅的な switch を組み合わせると、
「想定しているバリエーションを全部書く」
「増えたらコンパイルエラーで気づく」
という健全な状態を作れます。


sealed / final / non-sealed の使い分け

sealed の下に sealed を重ねる

階層をもう少し複雑にしてみます。

public sealed class Shape
        permits Circle, Quadrilateral {
}

public final class Circle extends Shape {
}

public sealed class Quadrilateral extends Shape
        permits Rectangle, Square {
}

public final class Rectangle extends Quadrilateral {
}

public final class Square extends Quadrilateral {
}
Java

ここでは、

  • Shape のサブタイプは CircleQuadrilateral に限定
  • Quadrilateral のサブタイプは RectangleSquare に限定

という 2 段階の sealed 階層になっています。

この構造を前提に switch を書くと、
コンパイラが「どこまで網羅されているか」をきちんと追いかけてくれます。

non-sealed で「ここから先は自由」を宣言する

「上の階層ではバリエーションを限定したいけど、
あるサブクラスから先は自由に継承していい」
というケースもあります。

そのときに使うのが non-sealed です。

public sealed class Shape
        permits Circle, CustomShape {
}

public final class Circle extends Shape {
}

public non-sealed class CustomShape extends Shape {
}
Java

CustomShapeShape のサブクラスですが、
non-sealed なので、そこから先は誰でも継承できます。

public class MyShape extends CustomShape {
}
Java

「ここまでは世界を閉じるけど、ここから先は開く」
というコントロールができるのが、non-sealed の役割です。


sealed class をどんなときに使うか

「閉じた世界のバリエーション」を表現したいとき

sealed class が一番ハマるのは、
「この概念には、取りうるパターンが有限個しかない」
という場面です。

例えば、

  • 課金プラン:Free / Standard / Premium
  • 結果型:Success / Failure
  • 形状:Circle / Rectangle / Triangle

など、「増やすとしても設計者のコントロール下で増やしたい」もの。

こういうときに sealed class を使うと、

  • 型として「バリエーションが有限である」ことを表現できる
  • switch で網羅性チェックが効く
  • 想定外のサブクラスをライブラリ利用者が勝手に作れない

というメリットが得られます。

enum との違いをどう考えるか

「有限個のバリエーションなら enum でよくない?」
という疑問も自然です。

ざっくり言うと、

  • enum:各定数が「1 個だけのインスタンス」でよいとき(状態を持たない or 持っても固定)
  • sealed class:各バリエーションごとに「普通のクラスとして状態やロジックを持たせたい」とき

という使い分けになります。

例えば、「課金プランごとに計算ロジックを変えたい」程度なら enum でもいけますが、
「各プランごとにフィールドやメソッドが全然違う」ような場合は、
sealed class でサブクラスを分けた方が自然です。


まとめ:sealed class を自分の言葉で説明するなら

あなたの言葉で sealed を説明すると、こうなります。

sealed クラスは、“このクラスを継承してよいサブクラスをあらかじめ列挙して、継承の世界を閉じる”ための仕組み。
permits で許可されたクラス以外は継承できず、サブクラス側は final / sealed / non-sealed のいずれかを必ず宣言する。
これによって、『この抽象型には取りうる具体型がこれだけある』とコンパイラに教えられるので、
switch 式で網羅性チェックが効き、想定外のサブクラスが紛れ込むことも防げる。
有限個のバリエーションを持つドメインを表現したいときに、enum よりリッチな選択肢として使える。」

タイトルとURLをコピーしました