【Java学習|実務向け】Java 17以降の設計力向上:sealedクラスとnon-sealedによる継承の精密制御

1. 導入:なぜ継承を制限する必要があるのか

オブジェクト指向設計において、「継承」は強力な武器ですが、無制限に許容すると「誰がどこでサブクラスを作ったか把握できない」という保守性の低下を招きます。Java 17で導入されたsealedクラス(封印クラス)は、継承可能なクラスをあらかじめ限定することで、安全な型階層を構築します。しかし、場合によっては「一部だけ継承を解放したい」というケースも発生します。そこで登場するのがnon-sealed modifierです。これを理解することで、堅牢かつ柔軟なドメインモデルを設計できるようになります。

2. 基礎知識:sealedとnon-sealedの役割

sealedクラスは、`permits`句を使って継承を許可するクラスを明示します。これにより、コンパイラは「サブクラスはこれらしか存在しない」と確定でき、switch式での網羅性チェック(exhaustive check)が可能になります。
一方、non-sealedは、その制限を解除する修飾子です。「このクラスはsealed階層の一部だが、ここから先は誰でも継承して良い」という境界線を引くために使用します。

3. 実装と解決策:階層の設計

sealedクラスを使用する場合、サブクラスには以下のいずれかの修飾子が必要です。
final:これ以上の継承を禁止する。
sealed:さらに継承を限定する。
non-sealed:継承制限を解除し、通常のクラスとして扱う。

この3つを組み合わせることで、意図的な継承設計が可能になります。

4. サンプルプログラム

以下は、決済処理を例にした設計例です。

// 決済の基底クラスを封印
public sealed interface Payment permits CreditCard, PayPal, Cash {
}

// finalで継承を完全に止める例
public final class CreditCard implements Payment {}

// sealedでさらに限定する例
public sealed interface PayPal permits PayPalExpress {}
public final class PayPalExpress implements PayPal {}

// non-sealedで継承を自由にする例(拡張性を担保)
public non-sealed class Cash implements Payment {}

// 実行クラス:switch式での網羅性チェックの恩恵
public class PaymentProcessor {
public String process(Payment p) {
// sealedのおかげでdefault句が不要(網羅性が保証される)
return switch (p) {
case CreditCard c -> “クレジットカード処理”;
case PayPal pp -> “PayPal処理”;
case Cash c -> “現金処理”; // non-sealedでも型としては認識される
};
}
}

5. 応用・注意点:現場での活用と落とし穴

・網羅性チェックの重要性
sealedクラスの最大のメリットは、switch式で全パターンを網羅しなければコンパイルエラーになる点です。これにより、将来的に継承クラスが増えた際、修正漏れをコンパイル時点で検知できます。

・non-sealedの乱用に注意
non-sealedを多用すると、せっかくの継承制限が無意味になります。現場では「ライブラリの利用者に継承させたい場合」や「拡張ポイントとして意図的に開けたい場合」など、明確な目的がある時にのみ使用してください。

・switch式とyield
複雑な条件分岐が必要な場合は、switch式の中で`yield`キーワードを使って値を返却します。`if-else`のネストを避けることで、可読性が劇的に向上します。

結論として、sealed/non-sealedを適切に使い分けることは、堅牢なAPI設計の第一歩です。まずは小規模なドメインモデルから導入し、コンパイラの恩恵を体感してみてください。

コメント

タイトルとURLをコピーしました