導入
Javaプログラミングにおいて、継承は強力な機能ですが、意図しないクラスまで継承されてしまうと設計の意図が崩れ、保守性が低下する原因となります。特にドメインモデルの定義において、「このクラスを継承できるのは特定のクラスだけに制限したい」というケースは多々あります。これを解決するのがJava 17で正式導入された「Sealed Classes(封印クラス)」と「permits」キーワードです。これにより、継承関係を厳格に制御し、型安全な設計を実現できます。
基礎知識
Sealed Classesは、そのクラスやインターフェースが「誰によって継承・実装されるか」を明示的に制限する仕組みです。これまでのJavaでは、アクセス修飾子による制限はできましたが、継承を完全に止めるにはfinalキーワードしかありませんでした。permitsキーワードを使用することで、開発者は「許可されたサブクラス」のみをリスト化し、それ以外のクラスからの継承をコンパイル時に禁止できるようになります。これは、switch式における網羅性チェック(exhaustive check)と組み合わせることで、非常に強力な制御フローを実現します。
実装/解決策
Sealed Classesを実装するには、クラス定義にsealed修飾子を付与し、permitsキーワードを使って許可するクラスを列挙します。また、許可されたサブクラス側では、final、sealed、またはnon-sealedのいずれかを明示する必要があります。これにより、継承階層が開発者の意図通りに管理されます。
サンプルプログラム
以下は、図形の面積を計算する処理をSealed Classesで実装した例です。
// sealedを使用して継承を許可するクラスを限定する
public sealed interface Shape permits Circle, Rectangle {
double area();
}
// 許可されたサブクラスはfinalを付けてこれ以上の継承を止めるのが一般的
public final class Circle implements Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI radius radius; }
}
public final class Rectangle implements Shape {
private final double width, height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
public double area() { return width height; }
}
// 活用例:switch式による網羅的な制御フロー
public class Main {
public static void main(String[] args) {
Shape shape = new Circle(5.0);
// 許可されたクラスが分かっているため、defaultを書かずに網羅できる
double result = switch (shape) {
case Circle c -> c.area();
case Rectangle r -> r.area();
};
System.out.println("面積: " + result);
}
}
応用・注意点
注意すべき点として、permitsで指定するサブクラスは、同一モジュール内(あるいは同一パッケージ内)に存在する必要があります。また、permitsで指定されたクラスは、必ず元となるsealedクラスを継承/実装しなければなりません。
現場でのポイントは、switch式との組み合わせです。もし将来的に新しいサブクラス(例:Triangle)を追加する場合、permitsに追加した瞬間に、既存のswitch式でコンパイルエラーが発生します。これにより、「新しい型が増えたのに処理を書き忘れる」というバグを未然に防ぐことができます。ドメイン駆動設計(DDD)における値オブジェクトやイベントの定義に活用すると、非常に堅牢なコードになります。

コメント