java-recipes

ホーム Record › Rec-04

Rec-04: Sealed Classes との組み合わせ

sealed interfacerecord を組み合わせると、 「種類が限定された型の集合」を型安全に表現できます。 Java 17 以降で利用可能なこのパターンは、switch パターンマッチングと組み合わせることで追加漏れをコンパイル時に検出できます。

sealed interface + record とは

「代数的データ型(ADT)」とは、「種類が決まった型の集合」を表す考え方です。 たとえば図形は「円・長方形・三角形のいずれか」と決まっています。 sealed interface を使うと、このような「種類が限定された型」をコンパイラに伝えることができます。

sealed interface の利点

  • 型の追加制限: permits で許可したクラス以外はサブタイプになれない
  • 網羅性チェック: switch パターンマッチングで全バリアントを処理しないとコンパイルエラーになる
  • record との相性: 各バリアントを record で定義すると値の保持も簡潔に書ける

Java バージョンごとの違い

  • Java 8: sealed / record が使えないため、abstract クラス + enum で近似実装します
  • Java 17: sealed interface と record が使えます。instanceof パターンマッチング(Java 16+)で型安全に処理できます
  • Java 21: switch パターンマッチングが正式化。sealed interface との組み合わせで全ケースの網羅性をコンパイラが保証します

サンプルコード

SealedRecordSample.java
public class SealedRecordSample {

    // Java 8: sealed の代替 - abstract class + enum type で表現
    enum ShapeType { CIRCLE, RECTANGLE, TRIANGLE }

    static abstract class Shape {
        abstract double area();
        abstract ShapeType getType();
    }

    static class Circle extends Shape {
        private final double radius;
        Circle(double radius) { this.radius = radius; }

        @Override
        public double area() { return Math.PI * radius * radius; }

        @Override
        public ShapeType getType() { return ShapeType.CIRCLE; }

        @Override
        public String toString() { return "Circle(radius=" + radius + ")"; }
    }

    static class Rectangle extends Shape {
        private final double width;
        private final double height;
        Rectangle(double width, double height) {
            this.width = width;
            this.height = height;
        }

        @Override
        public double area() { return width * height; }

        @Override
        public ShapeType getType() { return ShapeType.RECTANGLE; }

        @Override
        public String toString() { return "Rectangle(" + width + "x" + height + ")"; }
    }

    static class Triangle extends Shape {
        private final double base;
        private final double height;
        Triangle(double base, double height) {
            this.base = base;
            this.height = height;
        }

        @Override
        public double area() { return 0.5 * base * height; }

        @Override
        public ShapeType getType() { return ShapeType.TRIANGLE; }

        @Override
        public String toString() { return "Triangle(base=" + base + ", height=" + height + ")"; }
    }

    static String describe(Shape shape) {
        switch (shape.getType()) {
            case CIRCLE:
                return "円形: 面積 = " + String.format("%.2f", shape.area());
            case RECTANGLE:
                return "長方形: 面積 = " + String.format("%.2f", shape.area());
            case TRIANGLE:
                return "三角形: 面積 = " + String.format("%.2f", shape.area());
            default:
                return "不明な図形"; // Java 8 では default が必要
        }
    }

    public static void main(String[] args) {
        Shape[] shapes = {
            new Circle(5.0),
            new Rectangle(3.0, 4.0),
            new Triangle(6.0, 8.0)
        };
        for (Shape shape : shapes) {
            System.out.println(describe(shape));
        }
    }
}

Java 8 では sealed クラスが使えないため、abstract クラス + enum で「種類が限定された型」を表現します。ただし、この方法では新しいサブクラスを誰でも追加できてしまい、switch の default 漏れをコンパイル時に検出できないという弱点があります。

よくあるミス・注意点

⚠️ sealed interface のサブタイプはトップレベルか static ネストでなければならない

sealed interface の permits に指定できるのは、同じパッケージ内のトップレベルクラスか、static なネストクラスです。 メソッドの中で定義したローカルクラスや非 static な内部クラスは permits に指定できずコンパイルエラーになります。 各バリアントの record は必ず static なネストとして定義してください。

⚠️ switch に default を書くと網羅性チェックが無効になる

sealed interface との switch パターンマッチングに default ブランチを追加してしまうと、 新しいバリアントを permits に追加したときに switch の更新漏れがコンパイルエラーにならなくなります。 sealed interface との switch では default は極力書かないようにしましょう。

⚠️ sealed interface は Java 17+ の機能(Java 16 はプレビュー)

sealed interface は Java 17 で正式リリースされました。Java 16 ではプレビュー機能のため--enable-preview フラグが必要です。 Java 8 や Java 11 では使用できません。

テストする観点

  • Circle・Rectangle・Triangle それぞれの area() が正しい値を返すこと(境界値: radius=0 は面積 0)
  • describe() が各バリアントに対して正しい文字列を返すこと
  • sealed interface の permits に含まれていない型を instanceof チェックしようとするとコンパイルエラーになること
  • Java 21 の switch パターンマッチングで全バリアントが処理されること
  • 負の半径など不正な値に対するバリデーション(コンパクトコンストラクタでのチェック)が動作すること

GitHub でソースコードを見る →