導入
大規模なJavaアプリケーションにおいて、「特定のインターフェースの実装クラスを意図した範囲内に制限したい」と考えたことはありませんか。従来、アクセス修飾子やパッケージプライベートでの制御には限界がありました。Java 17で正式導入されたSealed Classes(シールクラス)は、継承階層を明示的に定義することで、コンパイラによる「網羅性チェック」を可能にします。これにより、if-elseの連鎖や不完全なswitch文によるバグを劇的に減らし、保守性の高いコードを実現できます。
基礎知識
Sealed Classesとは、許可されたサブクラス以外からの継承を禁止する機能です。`sealed`修飾子を付けたクラスやインターフェースは、`permits`句で継承可能なクラスを明示します。
これと相性が良いのが、Java 14以降で導入された「switch式」です。Sealedクラスとswitch式を組み合わせることで、コンパイラが「全てのサブクラスが網羅されているか」をチェックしてくれます。もし新しいサブクラスを追加した場合、対応していないswitch式はコンパイルエラーになるため、実装漏れを未然に防ぐことができます。
実装/解決策
実装のステップは以下の3点です。
1. 親クラスに`sealed`を付与し、`permits`でサブクラスを指定する。
2. サブクラスには`final`(継承終了)、`sealed`(さらに継承)、または`non-sealed`(制限解除)のいずれかを付与する。
3. 利用側ではswitch式を使用して、各サブクラスに対する処理を記述する。
サンプルプログラム
以下のコードは、決済処理を題材にした実用的な例です。
// 1. sealedインターフェースの定義
public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer {
}
// サブクラスは継承の性質を明示する必要がある
final class CreditCard implements PaymentMethod { public String getCardNumber() { return "1234-5678"; } }
final class PayPal implements PaymentMethod { public String getEmail() { return "user@example.com"; } }
final class BankTransfer implements PaymentMethod { public String getBankName() { return "J-Bank"; } }
public class PaymentProcessor {
public String process(PaymentMethod method) {
// 2. switch式による網羅的な処理
// もしpermitsに新しいクラスを追加し、ここを更新し忘れるとコンパイルエラーになる
return switch (method) {
case CreditCard c -> "カード決済: " + c.getCardNumber();
case PayPal p -> "PayPal決済: " + p.getEmail();
case BankTransfer b -> "銀行振込: " + b.getBankName();
// defaultが不要になるのがsealedクラスの最大のメリット
};
}
}
応用・注意点
現場で活用する際のポイントは以下の通りです。
default節の回避: switch式でSealedクラスを使う場合、`default`節を記述しないことを推奨します。`default`を書いてしまうと、新しいサブクラスを追加した際に「網羅性チェック」が働かなくなり、実行時に予期せぬ動作を招くリスクがあるからです。
non-sealedの使いどころ: ライブラリ開発などで、ユーザーに対して拡張の余地を残したい場合は`non-sealed`修飾子を使用します。これにより、継承の制限を意図的に解除できます。
設計の意図: Sealedクラスは「ドメインモデルの変更に弱い」という性質を逆手に取り、ビジネスルールの変更を型システムに強制的に反映させるための強力なツールです。むやみに全てのクラスに適用するのではなく、状態遷移や決済手段、権限管理など「型の網羅性が重要な箇所」に限定して導入するのが、シニアエンジニアとしての賢い設計判断と言えるでしょう。

コメント