---
title: "Javaのシールクラス（sealed class）とは？permitsと3修飾子・switch網羅・エラー例をJDK 25で実測"
url: "https://www.issoh.co.jp/tech/details/3219/"
published: 2024-08-01
updated: 2026-10-28
categories: ["Java"]
publisher: "株式会社一創"
---

# Javaのシールクラス（sealed class）とは？permitsと3修飾子・switch網羅・エラー例をJDK 25で実測

シールクラスは、継承してよいクラスを`permits`で名指しし、それ以外の継承をコンパイルエラーにするJavaの仕組みです。Java 17で正式機能になり、Java 21の`switch`パターンマッチと組み合わせると「全部の型を処理したか」をコンパイラが検査します。この記事では書き方と3つの修飾子に加え、実際に出るコンパイルエラーの文言、再コンパイルを忘れたときに実行時に起きる例外までを、Eclipse Temurin 25.0.4.1で`javac`と`java`を実行した結果で示します。

## まとめ：シールクラスの要点

- **書き方**：親に`sealed`を付け、`permits`で継承を許すクラスを列挙する。同じファイルに子を書くなら`permits`は省略できる。
- **子の修飾子**：許可された子は`final`・`sealed`・`non-sealed`のどれかが必須。`record`と`enum`は書かなくてよい。
- **置き場所**：親と子は同じモジュール、名前なしモジュールなら同じパッケージに置く。
- **網羅チェック**：Java 21以降で、抽象シールクラスまたはシールインターフェースを対象とする`switch`では、全サブタイプを`case`に書けば`default`が要らず、型を足し忘れるとコンパイルが止まる。`non-sealed`の子も、その子の型の`case`1つで子孫ごと扱える。
- **落とし穴**：`switch`側を再コンパイルせずに型を足すと実行時に`MatchException`。`default`を書くと追加した型が黙ってそこへ流れる。
- **向かない場面**：第三者に継承させる拡張点。公開ライブラリでは、許可サブタイプの追加が利用者の`switch`を壊す互換性問題になる。

## シールクラス（sealed class）とは：継承できる型をpermitsで閉じる仕組み

通常のクラスは、`final`でなく、クラスとコンストラクタにアクセスできれば、作者の知らないところでも継承できます。シールクラスはこれを逆にし、「直接継承してよいのはこのクラスだけ」と作者が宣言します。`final`（継承を全面禁止）と通常のクラス（無制限に開放）の中間にあたる選択肢です。

直接のサブタイプの集合が限定されるため、コンパイラはその集合を前提に検査できます。ただし、`non-sealed`の枝では、その先のサブタイプまで限定されません。後述の`switch`網羅チェックや、ありえないキャストの検出がそれです。仕様の一次情報は[JEP 409: Sealed Classes](https://openjdk.org/jeps/409)と[Java言語仕様 8.1.1.2](https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.1.1.2)です。

### Java 15のプレビューからJava 17の正式化まで

JEP 360でJDK 15にプレビューとして入り、JEP 397（JDK 16）の第2プレビューを経て、JEP 409でJDK 17の正式機能になりました。JDK 25の`javac`に`--release 16`を指定すると`シール・クラスは-source 16でサポートされていません`で止まり、`--release 17`なら通ります。

ただし`switch`のパターンマッチは[JEP 441](https://openjdk.org/jeps/441)でJDK 21の正式機能です。`--release 17`のままパターン付きの`case`を書くと`switch文のパターンは-source 17でサポートされていません`になります。シールクラスの網羅チェックまで使うなら、実質の下限はJava 21です。版ごとの違いは[Java 21と25の違い｜正式化した新機能・サポート期限・移行で止まる箇所を実機で比較](/tech/details/10212/)にまとめています。

## sealedとpermitsの書き方

次の`Vehicle`は`Car`と`Truck`にだけ直接の継承を許します。コードは`Vehicle.java`として保存し、`javac Vehicle.java`でコンパイルできます。

```
public abstract sealed class Vehicle permits Car, Truck { }

final class Car extends Vehicle { }
non-sealed class Truck extends Vehicle { }
class BigTruck extends Truck { }   // Truckはnon-sealedなので継承できる
```

インターフェースにも`sealed`を付けられ、その場合`permits`には実装クラスと子インターフェースを並べます。`sealed`・`permits`・`non-sealed`は特定の位置でだけキーワードになる語なので、`int sealed = 1, permits = 2;`のような既存の変数名もJDK 25でそのままコンパイルできます。

### 許可サブクラスに付ける3修飾子（final・sealed・non-sealed）

`permits`に挙げた子は、階層をその先どうするかを次の3つのどれかで必ず宣言します。`final`だけと説明する解説がありますが、3つのどれでもかまいません。

| 修飾子          | 意味            | その子をさらに継承           |
| ------------ | ------------- | ------------------- |
| `final`      | ここで打ち止め       | 不可                  |
| `sealed`     | さらに相手を限定して許可  | 自分の`permits`に挙げた型だけ |
| `non-sealed` | この枝だけ通常クラスに戻す | 誰でも可                |

どれも付けないと`sealed、non-sealedまたはfinal修飾子が必要です`（英語環境では`sealed, non-sealed or final modifiers expected`）になります。`non-sealed`は親が子に強制できない設計で、JEP 409も「シールクラスは許可サブクラスがこれを選ぶことを防げない」と書いています。なお、子がクラスでなくインターフェースの場合は`final`を付けられないので、[Java言語仕様 9.1.1.4](https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.1.1.4)のとおり`sealed`か`non-sealed`の2択になります。

### recordとenumを許可サブタイプにする場合

`record`は暗黙に`final`なので修飾子は不要です。`enum`もそのまま`sealed interface`を実装できます。次のコードはJDK 25で修飾子なしのままコンパイルが通り、`switch`では`enum`の定数を`case Unit.POINT`のように個別に書いても網羅と判定されました。

```
sealed interface Shape permits Circle, Square, Unit { }
record Circle(double r) implements Shape { }
record Square(double s) implements Shape { }
enum Unit implements Shape { POINT, EMPTY }
```

`record`の書き方そのものは[Javaのレコードクラス（record）とは？使い方・コンストラクタ・継承できない理由とエラー例をJava 25で実測](/tech/details/3254/)で扱っています。

### permitsを省略できる条件と置き場所の制約

許可したい子をすべて同じソースファイルに書くなら、`permits`は省略できます。コンパイラがそのファイル内で親を継承しているクラスを許可対象とみなします（Oracleの日本語ガイド[シール・クラス](https://docs.oracle.com/javase/jp/25/language/sealed-classes-and-interfaces.html)にも同じ説明があります）。別ファイルに分けるなら`permits`で列挙します。

置き場所には制約があります。親が名前付きモジュールにあれば子は同じモジュールに、名前なしモジュール（`module-info.java`を使わない普通のクラスパス構成）なら同じパッケージに置きます。パッケージ`a`の親が`b.Car`を許可しようとすると、`名前のないモジュールのクラスVehicleは別のパッケージのシール・クラスを拡張できません`で止まりました。パッケージをまたいで子を置きたいなら、`module-info.java`で名前付きモジュールにする必要があります。

## コンパイルエラーになる書き方と実際のメッセージ（JDK 25）

シールクラスの規則違反は、どれもコンパイル時に止まります。`javac`の表示言語はOSのロケールに従うので、日本語と英語の両方を載せます（英語は`javac -J-Duser.language=en`で出したもの）。検索するときはこの文言で探すと早く見つかります。

- **子に修飾子を付けていない**：`sealed、non-sealedまたはfinal修飾子が必要です` ／ `sealed, non-sealed or final modifiers expected`
- **permitsに無いクラスが継承した**：`クラスはシール・クラスVehicleを拡張できません('permits'句に指定されていないためです)` ／ `class is not allowed to extend sealed class: Vehicle (as it is not listed in its 'permits' clause)`
- **permitsに挙げたクラスが継承していない**：`無効なpermits句`（補足に`クラスTruckはシール・クラスを拡張する必要があります`） ／ `invalid permits clause`
- **メソッド内のローカルクラスが継承した**：`ローカルクラスはシール・クラスを拡張できません` ／ `local classes must not extend sealed classes`
- **匿名クラスで継承した**：`匿名クラスはシール・クラスを拡張できません` ／ `anonymous classes must not extend sealed classes`
- **switchで型が漏れている**：`switch式がすべての可能な入力値をカバーしていません` ／ `the switch expression does not cover all possible input values`

匿名クラスが使えないのは見落としやすい点です。テストで`new Vehicle() { }`のようにその場でスタブを作る書き方ができないため、テスト用の実装も`permits`に含めるか、許可サブクラスのどれかを`non-sealed`にして継承の余地を残すかを設計時に決めます。

## 継承・final・interfaceとの違い

### 通常の継承・finalとの選び方

判断基準は「サブタイプの集合を作者が握るかどうか」です。利用者に自由な拡張を求めるなら通常のクラスやインターフェース（考え方は[継承とは？ポリモーフィズムとの違い・委譲との使い分けをJavaとPythonのコードで解説](/tech/details/2724/)）、継承をまったくさせないなら`final`、種類は複数あるが有限に固定したいならシールクラスです。許可する子が1つも無いなら`final`で足ります。

### sealed classとsealed interfaceの使い分け

共通のフィールドや実装を親に持たせたいなら`sealed class`、振る舞いの型だけを決めて実装は各型に任せるなら`sealed interface`です。よく使われる組み合わせが`sealed interface`と`record`で、「取りうる値の種類を限定した型」を表せます。`record`はクラスを継承できないので、`record`を子にしたいなら親は必ず`sealed interface`になります。インターフェース全般の役割は[インターフェースとは？意味と種類・コードと通信の契約を実装目線で解説](/tech/details/17395/)を参照してください。

### シール型へのキャストとコンパイル時の拒否

シールにすると、キャストの判定も厳しくなります。次の`Box`は`final`ではないので、通常のインターフェース`Open`へのキャストは「`Box`のサブクラスが`Open`を実装するかもしれない」としてコンパイルが通ります。一方`Shape`の実装は`Circle`しかありえないので、`(Shape) b`は`不適合な型: BoxをShapeに変換できません`で止まりました。

```
sealed interface Shape permits Circle { }
final class Circle implements Shape { }
interface Open { }
class Box { }

class Casts {
    static Shape f(Box b) { return (Shape) b; }  // コンパイルエラー
    static Open  g(Box b) { return (Open) b; }   // 通る
}
```

JEP 409はこれを縮小参照変換（narrowing reference conversion）の定義変更として説明しています。実行するまで分からなかった`ClassCastException`の一部が、コンパイル時に見つかるということです。

## switchのパターンマッチと組み合わせた網羅チェック（Java 21以降）

Java 21の`switch`は、対象がシールされた型なら`permits`の一覧を見て網羅性を判定します。対象が抽象シールクラスまたはシールインターフェースなら、すべての許可サブタイプをガードなしの型パターンで扱うことで網羅できます。具象シールクラスでは親自身のインスタンスも対象になるため、親型の`case`や`default`が必要です（`sealed class A permits B`で`case B`だけを書くと、JDK 25でも網羅性エラーになりました）。網羅的な`switch`でも`null`は別扱いで、`case null`が無ければ`NullPointerException`になります。

```
sealed interface Shape permits Circle, Square { }
record Circle(double r) implements Shape { }
record Square(double s) implements Shape { }

class Geometry {
    static double area(Shape shape) {
        return switch (shape) {
            case Circle c -> Math.PI * c.r() * c.r();
            case Square s -> s.s() * s.s();
        };
    }
}
```

### 許可サブタイプ追加時のcase漏れとコンパイルエラー

あとから`Triangle`を`permits`に足すと、この`area`は`switch式がすべての可能な入力値をカバーしていません`でコンパイルが止まります。`Shape`で分岐している箇所がコードベースに何十か所あっても、直すべき場所をコンパイラが全部挙げてくれます。`if`と`instanceof`の連鎖では、この検査は働きません。

### defaultによる型追加時のcase漏れの見逃し

逆に、念のため`default -> 0.0;`を書いておくと、`Triangle`を足してもコンパイルは通り、`area(new Triangle(3, 4))`は黙って`0.0`を返しました。シールした型の`switch`に`default`を書くと、追加漏れの検出を自分で捨てることになります。全部の型を列挙できるなら`default`は書きません。

### non-sealedの枝全体を扱う型パターン

「許可サブクラスに`non-sealed`が混じると`default`が必要になる」という説明を見かけますが、JDK 25で試すと誤りでした。`Truck`が`non-sealed`で、その下に`BigTruck`がいても、`case Truck t`を1つ書けば`BigTruck`も含めて網羅になり、`default`なしで通ります。`fee(new BigTruck())`は`300`を返しました。

```
sealed interface Vehicle permits Car, Truck { }
final class Car implements Vehicle { }
non-sealed class Truck implements Vehicle { }
class BigTruck extends Truck { }

class Fees {
    static int fee(Vehicle v) {
        return switch (v) {
            case Car c   -> 100;
            case Truck t -> 300;   // BigTruckもここに入る
        };
    }
}
```

網羅にならないのは、`case Truck t`を書かずに`case BigTruck b`のように`non-sealed`の下の型だけを並べたときです。その下は誰でも継承できるので、`BigTruck`以外の子がありうるからです。

### レコードパターンのネストと\_（Java 22）

Java 21のレコードパターン（[JEP 440](https://openjdk.org/jeps/440)）を使うと、`record`の中身まで分解して分岐できます。ネストした場合も網羅性は検査され、`Add(Num a, Num b)`と`Add(Add a, Expr b)`だけでは「左が`Num`で右が`Add`」の組み合わせが漏れるので、コンパイルエラーになりました。

```
sealed interface Expr permits Num, Add, Neg { }
record Num(int v) implements Expr { }
record Add(Expr l, Expr r) implements Expr { }
record Neg(Expr e) implements Expr { }

class Exprs {
    static int eval(Expr e) {
        return switch (e) {
            case Num(int v)                  -> v;
            case Add(Num(var a), Num(var b)) -> a + b;
            case Add(var l, var r)           -> eval(l) + eval(r);
            case Neg(var x)                  -> -eval(x);
        };
    }

    static boolean isLeaf(Expr e) {
        return switch (e) {
            case Num _        -> true;
            case Add _, Neg _ -> false;   // Java 22以降
        };
    }
}
```

使わない変数を`_`で捨てる書き方は[JEP 456](https://openjdk.org/jeps/456)でJava 22の正式機能です。`--release 21`では`unnamed variables are not supported in -source 21`で止まるので、Java 21でプレビュー機能を使わない場合は、`case Num n`のように名前を付けます。また、複数パターンをまとめた分岐は、`case Add a -> false;`と`case Neg n -> false;`に分けます。`eval(new Add(new Num(2), new Neg(new Num(5))))`は`-3`を返しました。この`sealed interface`と`record`の組み合わせは、[Visitorパターンとは？ダブルディスパッチとJava・C#の実装例](/tech/details/3415/)で書いていた分岐を置き換える手段にもなります。

## 再コンパイルしないと実行時に落ちる：MatchExceptionとIncompatibleClassChangeError

網羅チェックはコンパイル時の保証です。型を足した側だけをビルドし直し、`switch`を書いた側のクラスファイルが古いまま動くと、保証は崩れます。Javaはそのとき黙って誤動作するのではなく、例外で止めます。

### switch側が古いままだとMatchException

`Shape`・`Circle`・`Square`と`area`を持つ`Main`を一度コンパイルし、そのあと`Shape`の`permits`に`Triangle`を足し、`Main`以外のクラスだけを再コンパイルしました。古い`Main.class`のまま`Triangle`を渡すと、次のように落ちます。

```
Exception in thread "main" java.lang.MatchException
	at Main.area(Main.java:3)
	at Main.main(Main.java:10)
```

`javap -c`で`Main.class`を見ると、`default`を書いていないのに`MatchException`を投げる命令が入っています。[MatchExceptionのAPIドキュメント](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/MatchException.html)はこの状況を「シールされたインターフェースの許可サブタイプが、コンパイル時と実行時で異なる場合」と名指ししています。`Main`も再コンパイルすれば、実行前にコンパイルエラーで気づけます。

### 未許可のサブクラスに対するJVMのロード時検査

親がシールされる前にコンパイルした子のクラスファイルは、`javac`の検査を通っていません。そこで実行時にも検査があります。`Vehicle`がまだ通常の抽象クラスだったときに`Bike extends Vehicle`をコンパイルし、あとで`Vehicle`だけを`sealed ... permits Car`に変えて動かすと、`Bike`のロード時に止まりました。

```
Exception in thread "main" java.lang.IncompatibleClassChangeError: Failed listed permitted subclass check: class Bike is not a permitted subclass of Vehicle
```

どちらも原因は同じで、シールされた型とそれを使うコードを別々にビルドしていることです。同じリポジトリなら全体を一緒にビルドすれば起きません。危ないのは、シールされた型をライブラリとして公開し、利用者が別のタイミングでビルドする場合です。既存の分岐で扱えない許可サブタイプを追加すると、利用者の網羅的な`switch`は再コンパイルでエラーになります。再コンパイルしないまま、その新しい型の値が未対応の`switch`に渡されると、実行時に`MatchException`になります。公開APIのシール型にサブタイプを足すのは、メジャーバージョンを上げるべき変更として扱ってください。

## Reflection API・javap・Jacksonによる許可サブクラスの取得

### isSealed()とgetPermittedSubclasses()の戻り値

JDK 17で`java.lang.Class`に`isSealed()`と`getPermittedSubclasses()`が追加されました。前述の`Shape`（`Circle`・`Square`・`enum Unit`を許可）で実行した結果です。

```
System.out.println(Shape.class.isSealed());                  // true
System.out.println(Arrays.toString(Shape.class.getPermittedSubclasses()));
                                                            // [class Circle, class Square, class Unit]
System.out.println(Circle.class.isSealed());                 // false（recordは暗黙のfinal）
System.out.println(String.class.getPermittedSubclasses());   // null
```

注意したいのは最後の行です。JEP 409の本文は「シールされていなければ空の配列を返す」と書いていますが、正式な[Class.getPermittedSubclasses()のAPI仕様](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Class.html#getPermittedSubclasses%28%29)は「シールされていなければ`null`」で、JDK 25の実行結果も`null`でした。なお、シール型で返される配列の要素順は仕様上未規定で、取得できなかった許可サブタイプは配列に含まれません。JEPを読んで`length`を直接呼ぶコードを書くと`NullPointerException`になるので、先に`isSealed()`で分けます。

クラスファイル側では、許可サブクラスは`PermittedSubclasses`属性として記録されます。`javap -v Shape`の出力にこの属性と`Circle`・`Square`の名前が並ぶので、ビルド済みのjarの中身がどちらの版の`Shape`かを確かめるときに使えます。

### Jackson 3によるシール型のサブタイプ自動検出

許可サブクラスの一覧を読み取って動くライブラリの例が、JSONライブラリのJacksonです。[jackson-databindのリリースノート](https://raw.githubusercontent.com/FasterXML/jackson-databind/3.x/release-notes/VERSION)によると、3.0系でJava 17のシール型からサブタイプを自動検出する機能（#5025）が入りました。ソースを見ると、`@JsonSubTypes`が無い型について`isSealed()`を確かめてから`getPermittedSubclasses()`の結果をサブタイプとして登録しています。jackson-databind 3.2.4で、`@JsonSubTypes`を書かずに試した結果です。Jackson 3はパッケージが`tools.jackson.databind`に変わりましたが、アノテーションは2系と同じ`com.fasterxml.jackson.annotation`を使います。

```
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.databind.json.JsonMapper;

@JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME)
sealed interface Payment permits Card, Bank { }
record Card(String last4) implements Payment { }
record Bank(String code) implements Payment { }

public class T {
    public static void main(String[] args) throws Exception {
        var mapper = JsonMapper.builder().build();
        System.out.println(mapper.writeValueAsString(new Card("4242")));
        // {"@type":"Card","last4":"4242"}
        Payment p = mapper.readValue("{\"@type\":\"Bank\",\"code\":\"0001\"}", Payment.class);
        System.out.println(p);
        // Bank[code=0001]
    }
}
```

型識別子の出し方を決める`@JsonTypeInfo`は従来どおり必要ですが、子を足すたびに`@JsonSubTypes`の一覧を直す作業は要らなくなります。同じコードをjackson-databind 2.22.4で動かすと、書き出しは通るものの読み込みで`InvalidTypeIdException`（`known type ids = []`）になりました。2系のままなら`@JsonSubTypes`を書きます。

## シールクラスを使うべき場面・避けるべき場面

使うべきなのは「種類が有限で、全種類を漏れなく扱いたい」型です。決済手段、APIのエラー種別、式や命令の構文木、注文の状態などがこれにあたります。JDK自身も`java.lang.constant.ConstantDesc`を`String`・`Integer`などに限定したシールインターフェースにしています。種類が増えたときに分岐の直し漏れをコンパイラに見つけさせたいなら、`sealed interface`と`record`で書き、`switch`に`default`を書かない、までを1セットにします。

避けるべきなのは次の3つです。1つ目は、フレームワークやプラグインのように第三者が新しい型を足す前提の拡張点です。2つ目は、前章で書いたとおり、別ビルドの利用者がいる公開ライブラリで、種類がまだ増えそうな型です。3つ目は、設計の初期で種類が週単位で変わる段階です。この段階で閉じると`permits`と置き場所の制約が変更のたびに手間になるので、種類が落ち着いてから閉じても遅くありません。「他人に継承させたくない」だけなら`final`で足ります。

業務システムでこうしたドメイン型の設計から相談したい場合は、弊社の[フルスクラッチ開発](/service/system/fullscratch/)でJavaによる設計・実装を支援しています。オブジェクト指向の設計全体での位置づけは[オブジェクト指向とは？三本柱の実装手順とrecord・sealedまでJavaコードで解説](/tech/details/13412/)にまとめています。

## よくある質問

### sealed classはJavaのどのバージョンから使えますか？

正式機能としてはJava 17（JEP 409）からで、JDK 15と16ではプレビュー扱いでした。ただし、シール型を対象にした`switch`のパターンマッチと網羅チェックが正式機能になったのはJava 21なので、シールクラスの利点をすべて使うならJava 21以降が前提です。

### permitsは必ず書く必要がありますか？

許可する子をすべて親と同じソースファイルに書く場合は省略できます。別ファイルに分けるときは`permits`で列挙し、子は同じモジュール（名前なしモジュールなら同じパッケージ）に置きます。

### non-sealedとは何ですか？

許可サブクラスに付ける修飾子の1つで、その子から先を通常のクラスと同じく誰でも継承できる状態に戻します。`switch`では、その子の型の`case`を1つ書けば、その枝の子孫も扱えます。他の許可サブタイプも網羅していれば、抽象シールクラスまたはシールインターフェースに対する`default`は不要です。

### C#のsealedとJavaのsealedは同じですか？

名前は同じでも意味が違います。[C#のsealed](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed)はそのクラスからの継承を完全に禁止する修飾子で、Javaの`final`にあたります。Javaの`sealed`は継承を許したうえで相手を`permits`で限定します。

### Kotlinのsealed classとの違いは何ですか？

[Kotlinのsealed class](https://kotlinlang.org/docs/sealed-classes.html)には`permits`が無く、直接の子は同じモジュールかつ同じパッケージに置いたものに限られます。シールクラス自体は常に抽象クラスで、子に`final`・`non-sealed`のような修飾子を付ける義務もありません。網羅チェックは`switch`ではなく`when`式で働きます。

## 関連記事

- [Javaのレコードクラス（record）とは？使い方・コンストラクタ・継承できない理由とエラー例をJava 25で実測](/tech/details/3254/)
- [オブジェクト指向とは？三本柱の実装手順とrecord・sealedまでJavaコードで解説](/tech/details/13412/)
- [Visitorパターンとは？ダブルディスパッチとJava・C#の実装例](/tech/details/3415/)
- [Java 21と25の違い｜正式化した新機能・サポート期限・移行で止まる箇所を実機で比較](/tech/details/10212/)
- [継承とは？ポリモーフィズムとの違い・委譲との使い分けをJavaとPythonのコードで解説](/tech/details/2724/)

---

出典: [Javaのシールクラス（sealed class）とは？permitsと3修飾子・switch網羅・エラー例をJDK 25で実測](<https://www.issoh.co.jp/tech/details/3219/>)（株式会社一創）
