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ここでのポイントは次の通りです。
Shapeはsealedpermits 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 {
}
JavaShape を設計した人が「Circle と Rectangle だけを想定していた」としても、
別の人が勝手に StrangeShape を作れてしまいます。
その結果、
- 「Shape のサブクラスはこの 2 つだけ」と思い込んで書いた
switchが破綻する - ライブラリの利用者が、想定外のサブクラスを作ってしまう
といった問題が起きます。
sealed class で「世界を閉じる」
sealed を使うと、「この継承階層の世界」を閉じることができます。
public sealed class Shape
permits Circle, Rectangle {
}
Javaこう書いた瞬間、Shape を継承できるのは Circle と Rectangle だけになります。
他のクラスが extends Shape と書こうとすると、コンパイルエラーです。
「この抽象型の具体的なバリエーションは、これとこれとこれだけ」
と、型レベルで宣言できるようになるわけです。
sealed class と switch 式の相性(網羅性チェック)
「全部のサブタイプを扱っているか?」をコンパイラが見てくれる
先ほどの Shape を使って、switch 式を書いてみます。
String describe(Shape shape) {
return switch (shape) {
case Circle c -> "円です";
case Rectangle r -> "四角です";
};
}
Javaここで default を書いていないのに、コンパイルは通ります。
なぜかというと、Shape のサブタイプは Circle と Rectangle だけだとコンパイラが知っているからです。
もし、Triangle というサブクラスを sealed 階層に追加したらどうなるか。
public sealed class Shape
permits Circle, Rectangle, Triangle {
}
Javaこの状態で先ほどの switch をコンパイルすると、
「Triangle を扱っていないよ」とコンパイルエラーになります。
これが 網羅性チェック です。
「将来サブクラスが増えたときに、対応漏れをコンパイラが教えてくれる」
というのは、設計としてかなり強力です。
default に逃げなくてよくなる
従来は、サブクラスが増えるかもしれない不安から、switch に default を書きがちでした。
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のサブタイプはCircleとQuadrilateralに限定QuadrilateralのサブタイプはRectangleとSquareに限定
という 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 {
}
JavaCustomShape は Shape のサブクラスですが、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 よりリッチな選択肢として使える。」
