【Java学習|豆知識】JavaのSealed Classesを使いこなす:final修飾子がもたらす堅牢な設計

導入:なぜSealed Classesにfinalが必要なのか

Java 17で正式導入されたSealed Classes(封印クラス)は、クラス階層を制限し、設計の意図をコンパイラに明確に伝えるための強力な機能です。しかし、「子クラスを定義する際、なぜfinal修飾子を付けるべきなのか」という疑問を持つ方は少なくありません。
このTipsは、継承の意図を明確化し、パターンマッチングやswitch式での網羅性チェックを確実に機能させるために不可欠な知識です。これを知ることで、予期せぬ拡張によるバグを防ぎ、コードの保守性を飛躍的に高めることができます。

基礎知識:Sealed Classesと制御フロー

Sealed Classesは、許可されたサブクラス以外からの継承を禁止します。ここで重要なのが、サブクラス側での宣言です。
サブクラスは、次のいずれかの修飾子を付ける必要があります。
final: これ以上継承させない(末端クラス)。
sealed: さらに階層を掘り下げて制限する。
non-sealed: 継承制限を解除し、誰でも継承できるようにする。

これらがなぜ重要かというと、Javaのswitch式における網羅性チェック(exhaustive check)と深く関わっているからです。コンパイラは、すべてのサブクラスが網羅されているかをチェックしますが、non-sealedクラスが含まれると、その先が未知であるため「デフォルトケース(default)」が必須となります。

実装・解決策:finalによる階層の終端

設計の原則として、拡張の必要がないクラスには積極的にfinalを付与すべきです。これにより、コンパイラは「このクラスが階層の末端である」と認識し、switch式でdefault句を省略しても安全であると判断できます。

サンプルプログラム:安全な階層設計とswitch式の活用

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

// 決済の型を制限するSealed Interface
sealed interface Payment permits CreditCard, PayPal {}

// finalを付与して末端であることを明示
final class CreditCard implements Payment {}
final class PayPal implements Payment {}

public class PaymentProcessor {
public String process(Payment payment) {
// switch式での網羅性チェック
// 全ての許可されたサブクラスがfinalのため、default句なしで安全に記述可能
return switch (payment) {
case CreditCard c -> {
yield “クレジットカードで処理します”;
}
case PayPal p -> {
yield “PayPalで処理します”;
}
};
}
}

応用・注意点:現場でのバグ回避

現場で最も陥りやすい罠は、「将来的に拡張するかもしれない」という安易な理由でnon-sealedを選択することです。

1. 拡張の意図を明確に: 本当に拡張を許可するならnon-sealedですが、そうでないなら必ずfinalを付けましょう。finalであれば、リファクタリング時に新しいサブクラスを追加した際、コンパイラが「switch式が網羅されていない」と警告を出してくれるため、修正漏れを確実に防げます。
2. デフォルトケースの誘惑: switch式でdefault句を安易に書くと、新しいサブクラスを追加した際にコンパイラがエラーを検知できなくなります。可能な限りdefault句を避け、finalクラスで階層を閉じることが、堅牢なコードへの第一歩です。

この設計手法を取り入れることで、複雑な条件分岐に悩まされることなく、コンパイラの力を借りた安全なアプリケーション開発が可能になります。ぜひ明日からの実装に取り入れてみてください。

コメント

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