CodeGym /コース /JAVA 25 SELF /sealed クラス: 構文と活用

sealed クラス: 構文と活用

JAVA 25 SELF
レベル 65 , レッスン 0
使用可能

1. sealed クラスの構文: どのように見えるか

まずは Java の OOP における古典的な問題、開かれた継承階層から始めましょう。通常の Java では、クラスが final でない限り、誰でもそのクラスを継承できます。便利ではありますが、思わぬ事態を招くことがあります — どのクラスがサブクラスになるのか事前には分からないため、switchif-else で全てのケースを確実に処理したと保証できません。

その結果、型に基づく処理(例えば switch のパターンマッチング)を書くとき、念のために default 分岐を追加せざるを得ません。どこかで誰かが新しいサブクラスを作るかもしれないからです。

sealed クラスはこの問題を解決します。継承可能なクラスの一覧を明示的に制限でき、階層を閉じてコントロール可能にします。これによりコードはより予測可能で安全になります。

基本構文

sealed クラスは Java 17 で導入されました。sealed 修飾子で宣言し、許可するサブクラスの一覧を permits で指定します。

public sealed class Shape permits Circle, Rectangle, Square {
    // すべての図形に共通の振る舞い
}

ここでは Shape クラスを宣言し、CircleRectangleSquare だけがその直接のサブクラスになれます。ほかの誰も Shape を拡張できません — コンパイラが許しません。

重要: permits に列挙した全てのクラスは、同じファイル内で宣言するか、コンパイラから可視でなければなりません(通常は同一パッケージ)。ちなみに、全てのサブクラスが sealed クラスと同じファイル内で宣言されている場合は、permits 自体を省略できます — コンパイラが自動的に判断します。

例:

// すべて同一ファイル内なら permits は省略可能
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}

サブクラスの要件

各サブクラスは自分のステータスを明示的に指定する必要があります:

  • final(それ以上の継承を禁止)にする、
  • もしくは sealed にして(さらに継承を制限する)、
  • もしくは non-sealed にして(継承を許可し、制約を解除する)。

例:

public sealed class Shape permits Circle, Rectangle, Square {}

public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
  • Circle — 最終クラスで、それ以上の継承は不可。
  • Rectangle — 自身が sealed で、2 つのサブクラスのみを許可。
  • Squarenon-sealed で、誰でも拡張可能。

最小の例

public sealed class Animal permits Dog, Cat {}

public final class Dog extends Animal {}
public final class Cat extends Animal {}

public class Wolf extends Animal {} のように新しいクラスを宣言してみると、コンパイルエラーになります:

class Wolf is not allowed to extend sealed class Animal

2. sealed クラスの用途: どこで、なぜ使うか

パターンマッチングと switch

sealed クラスは、switch のパターンマッチングと特に相性が良いです。コンパイラが可能なサブクラスを全て把握しているため、各ケースを処理していることを検証でき、default 分岐を要求しないこともあります。

public sealed interface Result permits Success, Error {}

public final class Success implements Result {
    public final String data;
    public Success(String data) { this.data = data; }
}

public final class Error implements Result {
    public final String message;
    public Error(String message) { this.message = message; }
}

public class Main {
    public static void main(String[] args) {
        Result result = new Success("やった!");
        switch (result) {
            case Success s -> System.out.println("成功: " + s.data);
            case Error e -> System.out.println("エラー: " + e.message);
        }
    }
}

コンパイラは Result に他のバリアントが存在しないことを知っているため、default を要求しません。どれかのケースを処理し忘れると、コンパイラがすぐに指摘します。

注意: Java 21 以降では、switch で全てのバリアントを処理していないとコンパイルエラーになります。より古いバージョン(17–20)では default が必要な場合がありますが、IDE は網羅性不足を警告します。

安全性とコントロール

sealed クラスにより、開発者は継承階層を完全にコントロールできます。これは、バリアントの集合が固定であるべきドメインモデル(例: 注文状態 NewPaidCancelled)において特に重要です。

保守と拡張の容易化

サブクラスの全バリアントが分かっていれば、新機能の追加、保守、リファクタリングが容易になります。IDE もすべてのバリアントを把握しているため、補完や解析で助けてくれます。

3. sealed クラスの実用例

例 1: 幾何学図形

public sealed interface Shape permits Circle, Rectangle, Square {}

public final class Circle implements Shape {
    public final double radius;
    public Circle(double radius) { this.radius = radius; }
}

public final class Rectangle implements Shape {
    public final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

public final class Square implements Shape {
    public final double side;
    public Square(double side) { this.side = side; }
}

これでパターンマッチング付きの switch を安全に使えます:

Shape shape = new Circle(5);
switch (shape) {
    case Circle c -> System.out.println("半径 " + c.radius + " の円");
    case Rectangle r -> System.out.println("長方形 " + r.width + "x" + r.height);
    case Square s -> System.out.println("一辺 " + s.side + " の正方形");
}

例 2: 金融トランザクション

public sealed interface Transaction permits Deposit, Withdraw, Transfer {}

public final class Deposit implements Transaction {
    public final double amount;
    public Deposit(double amount) { this.amount = amount; }
}

public final class Withdraw implements Transaction {
    public final double amount;
    public Withdraw(double amount) { this.amount = amount; }
}

public final class Transfer implements Transaction {
    public final double amount;
    public final String toAccount;
    public Transfer(double amount, String toAccount) {
        this.amount = amount;
        this.toAccount = toAccount;
    }
}

ハンドラー側で、どのトランザクション型も漏れなく処理していることを保証できます:

Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
    case Deposit d -> System.out.println("入金: " + d.amount);
    case Withdraw w -> System.out.println("出金: " + w.amount);
    case Transfer t -> System.out.println("振込: " + t.amount + " 宛先 " + t.toAccount);
}

例 3: non-sealed を使った制限付き階層

public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}

public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}

// これで誰でも SmsNotification を継承できる
public class ViberNotification extends SmsNotification {}

4. sealed クラスの特徴、制約、注意点

修飾子の要件

  • sealed クラスは、必ず permits で全てのサブクラスを明示します。
  • 全てのサブクラスは finalsealed、または non-sealed のいずれかである必要があります。
  • サブクラスは同じファイルで宣言するか、コンパイラから可視でなければなりません。

抽象 sealed クラス

sealed クラスは抽象クラスでも、interface でも、通常のクラスでも構いません。例えば:

public sealed abstract class Expr permits Const, Add, Mul {}

他の修飾子との併用

  • sealed クラスを finalnon-sealed と同時に宣言することはできません。
  • interfacesealed にできます(とても便利です)。

record クラスとの併用

record クラスは sealed クラスのサブタイプになれますが、その場合は final である必要があります(record はデフォルトで final)。

public sealed interface Expr permits Const, Add, Mul {}

public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}

5. 有用なポイント

sealed クラスとパターンマッチング: その関係

sealed クラスの肝は、網羅的なパターンマッチングです。難しく聞こえますが、動作は簡単です。コンパイラが取り得る全てのバリアントを知っているため、switchdefault なしで安心して書けます。

Expr expr = ...;
switch (expr) {
    case Const c -> ...
    case Add a -> ...
    case Mul m -> ...
}

どれかのバリアントを処理し忘れると、コンパイラがビルドを通させません — とても便利で安全です。

実務での適用例

sealed クラスが本当に役立つのは、固定されたバリアント集合がある場面です:

  • 操作結果: SuccessError(上の例のように)。
  • 注文状態: NewPaidCancelled
  • パーサ用の抽象構文木(AST)。
  • API レスポンス: OkNotFoundError
  • システム内のイベント: UserLoggedInUserLoggedOutUserRegistered

6. sealed クラスでよくあるミス

エラー No.1: permits に全てのサブクラスを列挙し忘れた。
必要なサブクラスを permits で列挙しないと、コンパイラは即座に不満を示します。例えば permits Circle, Rectangle と書いて Square を忘れたのに、そのクラスが存在する場合はエラーになります。

エラー No.2: サブクラスが finalsealed または non-sealed ではない。
サブクラスが必要な修飾子で宣言されていないと、コンパイラは「Class must be either final, sealed or non-sealed」とエラーを出します。

エラー No.3: サブクラスが別ファイルにあり、コンパイラから可視でない。
permits に列挙されたクラスはコンパイラから参照可能である必要があります — 同じファイル内、または同じパッケージに置きましょう。

エラー No.4: switch で全てのバリアントを処理していない。
switch のパターンマッチングで sealed クラスを使い、いずれかのバリアントを処理し忘れると、コンパイラはビルドを通しません。これは見落としを防げるので健全です。

エラー No.5: permits にない sealed クラスを継承しようとした。
permits に指定されていないサブクラスを作ろうとすると、「is not allowed to extend sealed class」というエラーになります。

エラー No.6: 古い JDK で sealed クラスを使おうとした。
sealed クラスは Java 17 で初めて導入されました。より古いバージョンで使おうとすると、コンパイルエラーになったり、そもそもビルドできなかったりします。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION