Contents
背景 ― JEP 409 が正式機能になるまでの流れ
JEP 409 とは何か
JEP 409 Sealed Classes は、型安全な継承階層を構築するために sealed キーワードと permits リスト、そして non‑sealed キーワードという三つの要素で成り立っています。
- sealed … 継承できる直接サブクラスをコンパイラが検証
- permits … 許可されたサブクラスを列挙(カンマ区切り)
- non‑sealed … sealed 階層から抜け、自由に継承可能にする
この仕組みは Java 17 でプレビューとして導入され、Kotlin の sealed クラスや Scala の final/sealed と同様の安全性を提供すると評価されました。
正式化に至った主な理由
| 要因 | 詳細 |
|---|---|
| 大規模プロジェクトでの継承誤用防止 | 企業レベルのコードベースでは、意図しないサブクラスが増えるとバグやセキュリティリスクが顕在化します。sealed により「許可された型だけ」が明示的に管理できます。 |
| 他言語との互換性確保 | Kotlin・Scala の開発者からは、同等の機能を Java でも欲しいという要望が多数寄せられました。 |
| コンパイラとランタイムの二重検証 | JEP 409 は javac と JVM の両方で許可リストの整合性をチェックする設計です。この二段階検証により、実行時エラーがほぼ排除されます。 |
| 実装コストの低減 | 既存の abstract クラスを sealed に変えるだけで機能が有効になるため、移行コストが低く抑えられました。 |
Oracle の公式ドキュメント(Sealed Classes and Interfaces – Java 21)でも「コンパイル時に許可されたすべてのサブタイプへアクセスできることが前提」と明記されています。
注:本稿中で参照している Qiita 記事(Java 21新機能まとめ #OpenJDK)は個人の解釈に基づく情報です。公式仕様が将来的に変更される可能性がありますので、最新情報は必ず Oracle のドキュメントをご確認ください。
基本構文とコンパイル要件
sealed, permits, non‑sealed キーワードの正しい使い方
|
1 2 3 4 5 6 |
// Shape.java package com.example.shapes; // 抽象クラスを sealed にし、許可サブクラスを列挙 public sealed abstract class Shape permits Circle, Square, Rectangle { } |
| 修飾子 | 意味 |
|---|---|
sealed |
直接のサブタイプは permits で列挙されたものだけに限定されます。 |
permits |
許可クラスをカンマ区切りで列挙します。同一モジュールか、適切にエクスポートされた別モジュールに存在する必要があります。 |
final |
継承不可。最終的な実装クラスに付与します。 |
non‑sealed |
sealed 階層から抜け、以降は自由に継承可能です。 |
実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Circle.java package com.example.shapes; public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } public double radius() { return radius; } } // Square.java package com.example.shapes; // non‑sealed にすると、Square のサブクラスを別モジュールで定義できる public non-sealed class Square extends Shape { private final double side; public Square(double side) { this.side = side; } public double side() { return side; } } // Rectangle.java package com.example.shapes; public final class Rectangle extends Shape { private final double width, height; public Rectangle(double w, double h) { this.width = w; this.height = h; } public double width() { return width; } public double height() { return height; } } |
module-info.java における可視性設定
sealed クラスとその許可クラスは 同一モジュール に置くか、別モジュールの場合は明示的にエクスポートしてアクセス可能にしなければコンパイルエラーになります。
|
1 2 3 4 5 |
// module-info.java(単一モジュール構成) module com.example.shapes { exports com.example.shapes; // Shape とすべての実装を公開 } |
別モジュールへ分割したいケース:
|
1 2 3 4 5 6 |
// module-shapes.jar の module-info.java module com.example.shapes { exports com.example.shapes.api; // API(Shape)だけを公開 exports com.example.shapes.impl to com.example.app; // 実装クラスは app に限定 } |
上記のように exports … to で対象モジュールを絞ることで、内部実装のカプセル化 と sealed の検証要件 を同時に満たすことができます。
Pattern Matching for switch との相乗効果
型安全な switch 式の書き方
Java 21 では Pattern Matching for switch が正式機能となり、sealed 階層と組み合わせると 網羅性チェック がコンパイラレベルで保証されます。
|
1 2 3 4 5 6 7 8 9 |
public static double area(Shape s) { return switch (s) { case Circle c -> Math.PI * c.radius() * c.radius(); case Square sq -> sq.side() * sq.side(); case Rectangle r -> r.width() * r.height(); // non‑sealed のサブクラスが追加されたらコンパイル時に警告が出ます }; } |
case Circle cなどの 型パターン がShapeの直接サブタイプとして認識され、すべて列挙しないと “switch expression does not cover all possible input values” エラーになります。- 将来的に
Squareをnon‑sealedにしたうえで新しいサブクラスTriangleが追加された場合、IDE とコンパイラが 未網羅 警告を出すため、忘れが防げます。
IDE のコード補完とリファクタリング支援
| IDE | 補完機能のポイント |
|---|---|
| IntelliJ IDEA (2023.2 以降) | switch (shape) { と入力すると自動で case Circle c ->, case Square s -> 等がポップアップ。未網羅ケースは Quick Fix から自動生成可能。 |
| Eclipse (2023‑12) | 「Ctrl+1」や「Quick Fix」で Missing case を検出し、テンプレートコードを挿入できる。 |
このように sealed が提供する型情報は IDE の解析エンジンに直接利用されるため、手書きミスが大幅に減少します。
ビルド・IDE 設定とサンプル実装(Shape 階層)
Maven と Gradle での標準設定
--enable-preview フラグは不要です。Java 21 をコンパイル対象に指定すれば sealed クラスはそのまま利用できます。
Maven (pom.xml)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <!-- module-info がある場合は自動で --module-path を付与 --> <release>21</release> </configuration> </plugin> </plugins> </build> |
Gradle (build.gradle)
|
1 2 3 4 5 6 7 8 9 10 11 |
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } tasks.withType(JavaCompile).configureEach { // module-info が存在すれば自動でモジュールパスを設定 options.release = 21 } |
IDE の最小構成チェックリスト
| IDE | 必要な設定項目 |
|---|---|
| IntelliJ IDEA | Project Structure → Project SDK を JDK 21、Language level も 21 に設定。Enable preview features は OFF(不要)。 |
| Eclipse | Preferences → Java → Compiler → JDK Compliance を 21 にし、Enable preview features のチェックは外す。 |
完全版サンプルコード
|
1 2 3 4 5 6 7 8 9 10 11 12 |
src └─ main └─ java └─ com └─ example └─ shapes │ Shape.java │ Circle.java │ Square.java │ Rectangle.java │ AreaCalculator.java |
|
1 2 3 4 5 6 7 8 9 |
// Shape.java package com.example.shapes; /** * 抽象的な図形を表す sealed クラス。 * 許可クラスは同一モジュール内に配置することが前提です。 */ public sealed abstract class Shape permits Circle, Square, Rectangle { } |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// Circle.java package com.example.shapes; /** * 円。半径は不変 (immutable) とし、getter を提供します。 */ public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } public double radius() { return radius; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// Square.java package com.example.shapes; /** * 正方形。非 sealed として、将来的に独自サブクラスを別モジュールで拡張可能です。 */ public non-sealed class Square extends Shape { private final double side; public Square(double side) { this.side = side; } public double side() { return side; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Rectangle.java package com.example.shapes; /** * 長方形。immutable な設計です。 */ public final class Rectangle extends Shape { private final double width, height; public Rectangle(double w, double h) { this.width = w; this.height = h; } public double width() { return width; } public double height() { return height; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// AreaCalculator.java package com.example.shapes; /** * Shape の面積を計算するユーティリティクラス。 * sealed クラスと pattern matching for switch により網羅性が保証されます。 */ public final class AreaCalculator { /** * 与えられた Shape の面積を返します。 * * @param s 計算対象の図形 * @return 面積(単位は入力に依存) */ public static double area(Shape s) { return switch (s) { case Circle c -> Math.PI * c.radius() * c.radius(); case Square sq -> sq.side() * sq.side(); case Rectangle r -> r.width() * r.height(); }; } } |
ポイント
-Shapeが sealed であるため、switch式は必ずすべてのサブタイプを列挙しなければコンパイルできません。
-Squareを non‑sealed にしたことで、別モジュールがTriangle extends Squareのように拡張可能です。その際はmodule-info.javaでエクスポート設定が必要になります。
移行ガイド・ベストプラクティス & アンチパターン
既存継承階層を段階的に sealed 化する手順
- 対象クラスを
abstractに変更し、sealed修飾子とpermitsを付与
java
// 変更前
public abstract class Message {}
// 変更後
public sealed abstract class Message permits TextMessage, ImageMessage {}
final
2. **サブクラスをまたはnon‑sealedに変換**switch 文の網羅性チェックは移行後に自動的に有効になるので、失敗したテストは即座に原因が判明します。
3. **module-info が存在すればエクスポート設定を見直す**(同一モジュールなら不要)
4. **テストスイートでコンパイルエラーが出ないことを確認**。特に
このプロセスは「安全な小さなステップ」で進められるため、リリースサイクルへの影響を最小限に抑えられます。
許可クラスは 最小限 に保つべき理由
| 目的 | 推奨策 |
|---|---|
| カプセル化 | permits には外部公開したいサブタイプだけを列挙し、内部実装は non‑sealed にしてモジュール内に閉じ込める。 |
| 保守性向上 | 許可リストが長くなると追加・削除の影響範囲が拡大します。できるだけ API レイヤー と 実装レイヤー を分離し、permits の対象は API に限定する。 |
| コンパイルエラーの検出効率化 | 変更があった際にコンパイラが即座に警告を出すので、意図しない依存関係が早期に判明します。 |
アンチパターン例(過剰な permits 列挙)
|
1 2 3 |
public sealed abstract class Service permits ImplA, ImplB, ImplC, ImplD, ImplE, ImplF, ImplG, ImplH { } |
- 問題点
- 公開 API が多数の実装に依存し、内部変更が外部へ波及しやすい。
-
新しい実装を追加するたびに
permitsを更新しなければならず、ミスが起きやすくなる。 -
改善策
Service自体はインタフェースか抽象クラスとして sealed にせず、公開 API 用のpublic interface Serviceとし、実装側を別パッケージに置いてnon‑sealedにする。
ベストプラクティスまとめ
| 項目 | 推奨手法 |
|---|---|
| 許可リスト | 公開向けサブタイプのみ列挙し、内部実装は non‑sealed にしてモジュール内に閉じ込める。 |
| モジュール化 | 同一モジュールにまとめるか、exports … to で必要最小限の可視性を付与する。 |
| テスト戦略 | switch 式の網羅性チェックを利用し、変更時にコンパイルエラーが出ることを安全装置として活用。 |
| ドキュメント | permits に列挙したクラスの役割・使用範囲は Javadoc で必ず記述する。 |
| IDE 活用 | IDE のコード補完と未網羅警告を積極的に利用し、手書きミスを防止する。 |
まとめと導入の呼びかけ
- Java 21 で sealed class が正式機能化(JEP 409)し、継承制御が言語レベルで保証されます。
sealed,permits,non‑sealedの構文と module-info.java によるアクセス設定を正しく行うだけで、コンパイルエラーや実行時不整合を防げます。- Pattern Matching for switch と組み合わせることで、網羅性がコンパイラにチェックされ、コードの安全性と可読性が大幅に向上します。
- Maven / Gradle の標準設定と最新 IDE(IntelliJ IDEA 2023.2+, Eclipse 2023‑12)を活用すれば、開発フローへの影響は最小です。
- 既存プロジェクトへの 段階的移行 と 許可クラスの最小化 を守ることで、リファクタリングコストやバグリスクを抑制できます。
ぜひ 本ガイドに沿って sealed class と pattern matching for switch をプロジェクトへ導入し、型安全な継承階層とメンテナンス性の高いコードベースを実現してください。