PR

Javaのシールクラス(Sealed Classes)とは?3分でわかりやすく解説

Java

Javaの新しい機能の1つとして注目されている「シールクラス(Sealed Classes)」は、クラスの継承を制限することでコードの安全性や明確性を高める仕組みです。「このクラスはこのクラスとこのクラスだけが継承できる」という制限があるクラス!というイメージ。

このページでは、Java初心者にも分かりやすいよう、シールクラスの基礎から応用までを1から順を追ってわかりやすく解説します。

シールクラスの特徴や利用方法を理解することで、より安全で保守しやすいJavaプログラムの実装が可能になります。

スポンサーリンク

Java:シールクラス(Sealed Classes)とは

シールクラスは、その名の通りクラスの継承を「封印」し、特定のクラスにのみ継承を許可できる機能です。

従来のJavaでは継承に制限がなく、意図しないサブクラスが派生してしまう可能性がありました。特に、大規模なプロジェクトでは、想定外のクラスが継承関係に入り込むと不具合の原因となり、デバッグが困難になることがあります。シールクラスを導入することで、あらかじめ指定したクラスだけが継承できるようになり、コードの保守性と可読性を向上させます。

なお、シールクラスはJava 15でプレビュー版として登場し、Java 17で正式リリースされました。そのため、実務で本格的に利用する場合はJava 17以上が推奨されます。

ポイント シールクラスが導入された背景

  1. 型安全性と保守性の向上
    自由な継承は、柔軟性と引き換えにコードの可読性や保守性を損なう場合があります。シールクラスを使うと「このクラスはこのクラスとこのクラスだけが継承できる」というように、範囲を限定して明示できます。これにより、想定外の拡張を防止できるだけでなく、どのクラスが継承関係にあるかを一目で把握できるようになります。
  2. パターンマッチングとの相性
    Java 17以降のパターンマッチング機能(特にswitch式の拡張)とシールクラスを組み合わせると、網羅性のチェックをコンパイラが行えます。継承クラスが限られているので、追加されたサブクラスがあればコンパイラが警告してくれることもあり、バグの早期発見に役立ちます。
  3. レコードクラスとの統合
    Java 16でレコード(Recordクラス)が導入され、データ専用のクラスを簡潔に表現できるようになりました。レコードにおいてもシールクラスと同様の発想で、データ構造の安全性や可読性を維持するための仕組みが活用されつつあります。

シールクラスの基本構文

シールクラスでは、クラス宣言に「sealed」を付け、続けて「permits」で継承を許可するクラスを列挙します。サブクラスは「final」「sealed」「non-sealed」のいずれかを指定しなければなりません。具体的には次の通りです。

sealed class Animal permits Dog, Cat {
}

final class Dog extends Animal {
}

non-sealed class Cat extends Animal {
}

  1. sealed 修飾子Animal クラスが sealed で宣言されているため、Animal を継承できるクラスは permits キーワードで指定された DogCat のみになります。
  2. final 修飾子Dogfinal なので、さらに継承できません。
  3. non-sealed 修飾子Catnon-sealed なので、さらに他のクラスが継承できます。

シールクラスを使った開発手順

ここからは、シールクラスを実際に使うときの手順を4つのステップに分けて紹介します。

ステップ1:Javaバージョンの確認

シールクラスは Java 17 で正式に導入されました(Java 15・16ではプレビュー機能)。利用するには、まず Java のバージョンをチェックします。以下のコマンドを実行して、openjdk version "17" などと表示されれば問題ありません。

java -version

ステップ2:シールクラスの宣言

まずは、シールクラスとして宣言するクラスに sealed を付与し、どのクラスが継承できるかを permits で指定します。たとえば、抽象的な図形を表す Shape クラスは以下のように書けます。

public sealed class Shape permits Circle, Square {
    public abstract double area();
}

このようにすることで、Shape クラスを継承できるのは CircleSquare のみになります。

ステップ3:サブクラスの実装

Shape を継承するクラスは「final」「sealed」「non-sealed」のいずれかで宣言しなければなりません。ここでは例として、CircleSquare を最終クラス(final)として定義します。

public final class Circle extends Shape {
    private double radius;

    public Circle(double r) {
        this.radius = r;
    }

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

public final class Square extends Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

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

ステップ4:動作の確認

最後に、メインメソッドなどで上記のクラスを使います。

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5.0);
        Shape square = new Square(4.0);

        System.out.println("Circle area: " + circle.area());
        System.out.println("Square area: " + square.area());
    }
}

ここで出力される円と正方形の面積を確認すれば、シールクラスが通常の抽象クラス同様に振る舞いつつ、継承制限が機能していることを把握できます。もし他のクラスが Shape を継承しようとするとコンパイルエラーが発生します。

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

シールクラスを継承するサブクラスは、次のいずれかの修飾子を必ず指定しなければいけません。

  1. sealed
    継承をさらに制限しつつ、サブクラスに委譲したい場合に使います。例えば、「このクラスを継承できるのは、次の2つのクラスだけ」というように再度制限をかけられます。
  2. non-sealed
    シールクラスから継承したクラスが、再び自由に継承を許可したい場合は non-sealed を指定します。特定の段階まで継承を制限し、それ以降はフリーにする、といった柔軟な階層設計を可能にします。
  3. final
    これ以上サブクラスを作らせない場合に用います。最終的な実装クラスや、「もうこれ以上の拡張は必要ない」というクラスに適しています。

実用例(支払い方法のモデル化)

シールクラスは、抽象的な概念と具体的な実装を明確に分けたいときに役立ちます。たとえば、支払い方法をモデル化したい場合、以下のように設計が可能です。

public sealed class PaymentMethod permits CreditCard, BankTransfer, Cash {
    public abstract void pay(double amount);
}

public final class CreditCard extends PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("クレジットカードで " + amount + " 円を支払いました。");
    }
}

public final class BankTransfer extends PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("銀行振込で " + amount + " 円を支払いました。");
    }
}

public final class Cash extends PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("現金で " + amount + " 円を支払いました。");
    }
}

シールクラスとパターンマッチング

Java 17 以降では、パターンマッチング(特に拡張された switch 式)が強化されており、シールクラスと組み合わせることでコードの安全性と可読性をさらに高められます。以下のような switch 式を書いた場合、サブクラスがすべて列挙されていないとコンパイラが警告を出すことがあります。

public void processPayment(PaymentMethod method, double amount) {
    switch (method) {
        case CreditCard c -> c.pay(amount);
        case BankTransfer b -> b.pay(amount);
        case Cash cash -> cash.pay(amount);
    }
}

シールクラスにより継承可能なクラスが限定されているおかげで、「万が一サブクラスが増えたらどうしよう」という不安を減らせます。

まとめ Javaのシールクラス

シールクラスは、Java の継承を厳格にコントロールできる新機能であり、想定外の拡張を防ぐことでコードの安全性や保守性を高める手段です。特に大規模システムやライブラリ開発では、クラス階層の複雑化を抑えるうえで非常に有効です。

  • 想定外の継承防止
    どのクラスが継承できるかをあらかじめ明示するため、バグの原因となりやすいクラス階層の肥大化を防ぎます。
  • パターンマッチングとの親和性
    シールクラスと switch 式を組み合わせると、すべてのサブクラスを網羅的に扱うことができ、抜け漏れをコンパイラが検知してくれます。
  • ライブラリ開発の安定性
    API 提供者が望む以外の方法でクラスが拡張されないため、予期せぬ使われ方による不具合を低減できます。
  • 保守性の向上
    継承関係が明示化されるため、コードレビューやメンテナンス時に「なぜこのクラスが継承されているのか」を容易に把握できます。
タイトルとURLをコピーしました