Javaのsealedクラス(シールクラス)とは、「このクラスを継承してよいのは、ここに書いたクラスだけ」と宣言できる仕組みです。 sealed interface Shape permits Circle, Square { } のように、sealed で継承を制限し、permits のあとに許可する型を並べます。継承できる型が決まっているので、switch で分岐するときにコンパイラが「全部の型を書いたか」を確かめてくれます。

この記事では、sealed・permits・final・non-sealed の役割を表にまとめ、いちばん小さなコードから順に積み上げていきます。permits にないクラスを継承したときや、switch の分岐が足りないときのコンパイルエラーは、OpenJDK 21.0.10 で再現したメッセージをそのまま載せました。Java 17 で使える範囲と Java 21 で使える範囲もバージョン別に整理し、default が不要になる理由を出力つきで確かめます。

sealedクラスの早見表

4つのキーワードの役割

キーワード 付ける場所 役割 書かないとどうなるか
sealed 親のクラス・インターフェース 継承・実装できる型を限定する だれでも継承できる通常の型になる
permits 親の宣言の末尾 継承・実装を許可する型を並べる 同じファイル内のサブクラスが自動で許可される
final 許可されたサブクラス これ以上は継承させない 3つのうちどれかを書かないとコンパイルエラー
sealed(サブクラス側) 許可されたサブクラス さらに下の階層も限定する 同上
non-sealed 許可されたサブクラス ここから先は自由に継承してよい 同上

許可されたサブクラスは、final・sealed・non-sealed のどれか1つを必ず書きます。 record と enum は最初から final 扱いなので、書かなくてもそのまま使えます。

バージョン別にできること

機能 使えるバージョン 書き方の例
sealed クラス・インターフェース、permits、non-sealed Java 17 以降 sealed interface Shape permits Circle, Square { }
record Java 16 以降 record Circle(double radius) implements Shape { }
instanceof のパターンマッチング Java 16 以降 if (s instanceof Circle c) { ... }
switch のパターンマッチング(型で分岐) Java 21 以降 case Circle c -> ...
record パターン(中身を取り出す) Java 21 以降 case Circle(double radius) -> ...
switch の case null Java 21 以降 case null -> ...

sealed そのものは Java 17 から、sealed と switch を組み合わせて「default なしで全部の型を分岐する」書き方は Java 21 から使えます。この区切りは、記事の後半で javac --release を使って確かめます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java sealedクラスの使い方|permitsとswitchの書き方」

検証した環境

項目 内容
JDK OpenJDK 21.0.10
実行方法 java ファイル名.java(単一ファイル実行)
コンパイルエラーの確認 javac ファイル名.java
バージョンごとの確認 javac --release 16・--release 17・--release 20 で、同じ JDK から各バージョン向けにコンパイル

許可されるサブクラスの制約(同じモジュールまたは同じパッケージに置くこと、final・sealed・non-sealed のいずれかを書くこと、permits を省略できる条件)は、Oracle の Java 21 言語ガイド日本語版「シール・クラス」のページを今回開いて確認しました。Java 22 以降の JDK での動作は未実行です。

ステップ1:sealedとpermitsの最小コード

まずは、インターフェース1つと record 3つの、いちばん小さな形です。次のコードはそのまま Step1.java として保存して動かせます。

public class Step1 {

    // Shape を実装できるのは Circle・Square・Rect の3つだけ
    sealed interface Shape permits Circle, Square, Rect { }

    record Circle(double radius) implements Shape { }
    record Square(double side) implements Shape { }
    record Rect(double width, double height) implements Shape { }

    // 型ごとに分岐する(default は書かない)
    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.radius() * c.radius();
            case Square q -> q.side() * q.side();
            case Rect r -> r.width() * r.height();
        };
    }

    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Square(2), new Rect(2, 3) };
        for (Shape s : shapes) {
            System.out.printf("%s 面積=%.2f%n", s, area(s));
        }
        System.out.println(Shape.class.isSealed());
        for (Class<?> c : Shape.class.getPermittedSubclasses()) {
            System.out.println(c.getSimpleName());
        }
    }
}

実行結果(java Step1.java):

Circle[radius=1.0] 面積=3.14
Square[side=2.0] 面積=4.00
Rect[width=2.0, height=3.0] 面積=6.00
true
Circle
Square
Rect

ここでのポイントは3つです。

  • sealed interface Shape permits Circle, Square, Rect で、Shape を実装できる型を3つに限定しています。
  • record は暗黙的に final なので、final を書かなくても許可されたサブクラスの条件を満たします。
  • switch に default がありません。 3つの型をすべて書いたので、コンパイラが「これで全部」と判断できています。

最後の4行は、Class クラスのメソッドで sealed の情報を取り出した結果です。isSealed() が true、getPermittedSubclasses() が permits に書いた順の3つを返しています。

permitsは省略できる

親と許可されたサブクラスを同じファイルに書くときは、permits を省略できます。 コンパイラが同じファイルの中から、その型を直接継承しているクラスを探して、許可されたサブクラスとして扱います。

// Task.java の中にすべて書く場合
sealed interface Task { }                       // permits を書かない
record Todo(String title) implements Task { }
record Done(String title) implements Task { }
書き方 向いている場面
permits を明示する サブクラスを別のファイルに分ける。許可する型を宣言の1行で一覧したい
permits を省略する 小さな型を1ファイルにまとめる。record を並べるだけの簡潔な定義

sealed と書いたのにサブクラスが1つもないと、次のコンパイルエラーになります。

Err5.java:1: error: sealed class must have subclasses
sealed class Lonely { }
       ^

ステップ2:final・sealed・non-sealedで階層を作る

次は、クラスで階層を作る例です。許可されたサブクラスに、3つの修飾子をそれぞれ使い分けます。

public class Mods {

    // 親:Todo・Recurring・Custom の3つだけが継承できる
    sealed static abstract class Task permits Todo, Recurring, Custom { }

    // final:ここで継承を終える
    static final class Todo extends Task { }

    // sealed:さらに下の階層も2つに限定する
    sealed static class Recurring extends Task permits Daily, Weekly { }
    static final class Daily extends Recurring { }
    static final class Weekly extends Recurring { }

    // non-sealed:ここから先は自由に継承してよい
    static non-sealed class Custom extends Task { }
    static class MyCustom extends Custom { }

    static String kind(Task t) {
        return switch (t) {
            case Todo x -> "一回きり";
            case Daily x -> "毎日";
            case Weekly x -> "毎週";
            case Recurring x -> "その他の繰り返し";
            case Custom x -> "自由拡張(" + x.getClass().getSimpleName() + ")";
        };
    }

    public static void main(String[] args) {
        System.out.println(kind(new Todo()));
        System.out.println(kind(new Daily()));
        System.out.println(kind(new Weekly()));
        System.out.println(kind(new Recurring()));
        System.out.println(kind(new MyCustom()));
    }
}

実行結果:

一回きり
毎日
毎週
その他の繰り返し
自由拡張(MyCustom)

階層を図にすると次のようになります。

classDiagram
    class Task {
        <<sealed abstract>>
    }
    class Todo {
        <<final>>
    }
    class Recurring {
        <<sealed>>
    }
    class Daily {
        <<final>>
    }
    class Weekly {
        <<final>>
    }
    class Custom {
        <<non-sealed>>
    }
    Task <|-- Todo
    Task <|-- Recurring
    Task <|-- Custom
    Recurring <|-- Daily
    Recurring <|-- Weekly
    Custom <|-- MyCustom
修飾子 この例のクラス その先の継承 switch での扱い
final Todo、Daily、Weekly できない その型を書けば網羅できる
sealed Recurring permits に書いた型だけ 下の型をすべて書くか、Recurring 自体を書く
non-sealed Custom 自由にできる Custom を書けば、その下の型もまとめて受けられる

Recurring は abstract ではないので、new Recurring() というインスタンスも作れます。そのため、Daily と Weekly を書いたあとに case Recurring x も必要です。下の型を先に、上の型を後に書くのが順番のルールで、逆にすると後の節で紹介する「dominated」のエラーになります。

non-sealed は、「親の型としては種類を限定したいが、この枝だけは利用者が自由に拡張できるようにしたい」ときに使います。MyCustom は permits のどこにも書かれていませんが、Custom が non-sealed なので継承できています。

permitsにないクラスを継承したときのコンパイルエラー

ここからは、ルールに合わない書き方をしたときのメッセージを1つずつ再現します。メッセージの形を知っておくと、出会ったときにすぐ直せます。

許可されていない型が継承・実装した

sealed interface Shape permits Circle, Square { }
record Circle(double radius) implements Shape { }
record Square(double side) implements Shape { }
record Triangle(double base, double height) implements Shape { }
Err1.java:4: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
record Triangle(double base, double height) implements Shape { }
^
1 error

直し方は、permits に Triangle を足すことです。 足した瞬間に、Shape を分岐しているすべての switch で「Triangle の分岐がない」とコンパイラが知らせてくれるようになります(次の節で再現します)。

サブクラスに final・sealed・non-sealed を書いていない

sealed class Animal permits Dog, Cat { }
final class Dog extends Animal { }
class Cat extends Animal { }
Err2.java:3: error: sealed, non-sealed or final modifiers expected
class Cat extends Animal { }
^
1 error

そのほかのエラー一覧

書いたコード コンパイルエラーのメッセージ 直し方
permits にない型が実装した class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause) permits に足す
サブクラスに修飾子がない sealed, non-sealed or final modifiers expected final・sealed・non-sealed のどれかを書く
sealed なのにサブクラスがない sealed class must have subclasses サブクラスを作るか sealed を外す
permits に書いた型が親を継承していない invalid permits clause (subclass Q must extend sealed class) サブクラス側に extends・implements を書く
sealed でない型のサブクラスに non-sealed を書いた non-sealed modifier not allowed here (class X does not have any sealed supertypes) non-sealed を外す
別のパッケージのクラスを permits に書いた(モジュールなし) class Shape in unnamed module cannot extend a sealed class in a different package 同じパッケージに置く、またはモジュールにまとめる
sealed な型を匿名クラスで実装した anonymous classes must not extend sealed classes 名前のあるクラスを permits に足す

いずれも今回 javac で再現したメッセージです(表ではカッコ内の補足行を1行にまとめています)。

許可されたサブクラスを置ける場所

Oracle の言語ガイドには、許可されたサブクラスの置き場所について次の制約が書かれています。

親の sealed クラスがある場所 許可されたサブクラスを置ける場所
名前付きモジュールの中 同じモジュールの中(パッケージは別でもよい)
名前のないモジュール(module-info.java を使わない構成) 同じパッケージの中

module-info.java を使っていない構成で、パッケージ a の Shape がパッケージ b の Square を permits に書いたところ、上の表の6行目のエラーになりました。名前付きモジュールの中で別パッケージに置く形は、今回は動かしていません(未実行)。

switchで分岐が足りないときのコンパイルエラー

sealed のいちばんの利点は、switch の分岐の書き漏れをコンパイラが見つけてくれることです。3つの型を許可した sealed インターフェースに対して、2つしか分岐を書かなかった例を見ます。

sealed interface Result permits Ok, Ng, Pending { }
record Ok(String value) implements Result { }
record Ng(String reason) implements Result { }
record Pending() implements Result { }

class Use {
    // switch 式
    static String show(Result r) {
        return switch (r) {
            case Ok ok -> "成功 " + ok.value();
            case Ng ng -> "不成立 " + ng.reason();
        };
    }
    // switch 文
    static void stmt(Result r) {
        switch (r) {
            case Ok ok -> System.out.println("ok");
            case Ng ng -> System.out.println("ng");
        }
    }
}

javac Err3.java の出力:

Err3.java:7: error: the switch expression does not cover all possible input values
        return switch (r) {
               ^
Err3.java:13: error: the switch statement does not cover all possible input values
        switch (r) {
        ^
2 errors

値を返す switch 式でも、返さない switch 文でも、型のパターンで分岐していれば網羅のチェックが働きます。 case Pending p -> ... を足すと、どちらもコンパイルが通ります。

defaultが不要になる理由を出力で確かめる

default を書いても、コンパイルは通ります。では、書かないほうがよいのはなぜでしょうか。同じ Result に対して、default ありと default なしの2つのメソッドを用意して比べます。

public class Def {
    sealed interface Result permits Ok, Ng, Pending { }
    record Ok(String value) implements Result { }
    record Ng(String reason) implements Result { }
    record Pending() implements Result { }

    // default あり:Pending の分岐を書いていない
    static String withDefault(Result r) {
        return switch (r) {
            case Ok ok -> "成功 " + ok.value();
            case Ng ng -> "不成立 " + ng.reason();
            default -> "想定外";
        };
    }

    // default なし:3つすべてを書いている
    static String noDefault(Result r) {
        return switch (r) {
            case Ok ok -> "成功 " + ok.value();
            case Ng ng -> "不成立 " + ng.reason();
            case Pending p -> "保留中";
        };
    }

    public static void main(String[] args) {
        System.out.println(withDefault(new Pending()));
        System.out.println(noDefault(new Pending()));
    }
}

実行結果:

想定外
保留中

withDefault は、Pending の分岐を書いていないのにコンパイルが通り、実行すると 想定外 に流れました。default があると、「残りは全部ここで受ける」という意味になるため、コンパイラは書き漏れを知らせる理由がなくなります。

比べる点 default あり default なし
型を1つ書き忘れたとき コンパイルは通り、実行時に default へ流れる コンパイルエラーで気づける
あとから permits に型を足したとき コンパイルは通り、既存の switch は default へ流れる すべての switch がコンパイルエラーになり、直す場所がわかる
向いている場面 「その他」をまとめて扱うのが本当に意図どおりのとき 型ごとに処理を書き分けたいとき

つまり、sealed で型が限定されているから default なしで網羅でき、default を書かないから型の追加に気づける、という関係です。sealed と switch は、この組み合わせで力を発揮します。

caseの順番で出る「dominated」エラー

case は上から順に調べられます。広い型を先に書くと、後ろの case に到達できなくなるため、コンパイラが知らせてくれます。

sealed interface S permits A, B { }
record A() implements S { }
record B() implements S { }

class U {
    static String f(S s) {
        return switch (s) {
            case S any -> "全部";
            case A a -> "A";
        };
    }
}
Err8.java:8: error: this case label is dominated by a preceding case label
            case A a -> "A";
                 ^

具体的な型を上に、広い型を下に並べ替えると解決します。ステップ2で Daily・Weekly を Recurring より先に書いたのは、このためです。

switch に null を渡したとき

型のパターンで分岐する switch でも、case null を書いていなければ、null を渡すと NullPointerException になります。先ほどの noDefault(null) を呼んだところ、NullPointerException(メッセージは null)になりました。null もありうる値として扱いたいときは、case null -> ... を1行足します。

Java 17とJava 21で書けることの違い

sealed は Java 17 で使えるようになりましたが、switch のパターンマッチングは Java 21 からです。同じ JDK 21 の javac に --release オプションを付けて、各バージョン向けにコンパイルした結果で確かめます。

Java 16 向け:sealed がまだ使えない

R17b.java:2: error: sealed classes are not supported in -source 16
    sealed interface Shape permits Circle, Square { }
    ^
  (use -source 17 or higher to enable sealed classes)

Java 17 向け:sealed は使えるが、switch の型パターンはまだ使えない

public class R17 {
    sealed interface Shape permits Circle, Square { }
    record Circle(double radius) implements Shape { }
    record Square(double side) implements Shape { }

    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.radius() * c.radius();
            case Square q -> q.side() * q.side();
        };
    }
}

javac --release 17 R17.java の出力:

R17.java:7: error: patterns in switch statements are not supported in -source 17
            case Circle c -> Math.PI * c.radius() * c.radius();
                 ^
  (use -source 21 or higher to enable patterns in switch statements)
1 error

--release 20 でも同じメッセージになり、record パターンについては deconstruction patterns are not supported in -source 20 (use -source 21 or higher to enable deconstruction patterns) と表示されました。正式に使えるのは Java 21 からであることが、コンパイラのメッセージからも読み取れます。

Java 17 での書き方:instanceof で分岐する

Java 17 では、instanceof のパターンマッチング(Java 16 以降)で分岐します。

public class R17b {
    sealed interface Shape permits Circle, Square { }
    record Circle(double radius) implements Shape { }
    record Square(double side) implements Shape { }

    static double area(Shape s) {
        if (s instanceof Circle c) return Math.PI * c.radius() * c.radius();
        if (s instanceof Square q) return q.side() * q.side();
        throw new IllegalStateException("未対応: " + s);
    }

    public static void main(String[] a) {
        System.out.println(area(new Square(3)));
    }
}

javac --release 17 でコンパイルして実行した結果は 9.0 でした。

比べる点 Java 17(instanceof で分岐) Java 21(switch で分岐)
継承できる型の制限 できる できる
分岐の書き漏れ コンパイラは知らせない。最後の throw で実行時にわかる コンパイルエラーで知らせる
最後の throw や default 必要 不要
record の中身の取り出し c.radius() とアクセサを呼ぶ case Circle(double radius) で直接取り出せる

Java 17 でも「許可した型以外は存在しない」という保証は得られます。Java 21 にすると、そこへ「分岐の網羅をコンパイラが確かめる」が加わります。

recordパターンで中身を取り出す(Java 21)

Java 21 では、case の中で record の中身を変数に取り出せます。これを record パターンと呼びます。ステップ1の Shape に、説明文を作るメソッドを足します。

static String describe(Shape s) {
    return switch (s) {
        case Circle(double radius) -> "半径 " + radius + " の円";
        case Square(double side) -> "一辺 " + side + " の正方形";
        case Rect(double w, double h) when w == h -> "正方形に見える長方形 " + w;
        case Rect(double w, double h) -> w + " x " + h + " の長方形";
    };
}

new Circle(1)、new Square(2)、new Rect(2, 3)、new Rect(4, 4) を渡した実行結果:

半径 1.0 の円
一辺 2.0 の正方形
2.0 x 3.0 の長方形
正方形に見える長方形 4.0
書き方 意味
case Circle c 型が Circle なら、変数 c に入れる(型パターン)
case Circle(double radius) 型が Circle なら、中身の radius を取り出す(record パターン)
case Rect(double w, double h) when w == h さらに条件を付ける(when はガードと呼ぶ)

when の付いた case は「条件に合ったときだけ」なので、網羅の計算には数えられません。 上の例では、条件なしの case Rect(double w, double h) が後ろにあるので、Rect 全体が網羅されています。条件付きの case を上に、条件なしを下に置く順番も大切です。

入れ子の record を再帰でたどる

record パターンは入れ子にできるので、式の木のような構造を短く扱えます。

public class Gen {

    sealed interface Expr permits Num, Add, Mul { }
    record Num(int value) implements Expr { }
    record Add(Expr left, Expr right) implements Expr { }
    record Mul(Expr left, Expr right) implements Expr { }

    static int eval(Expr e) {
        return switch (e) {
            case Num(int v) -> v;
            case Add(Expr l, Expr r) -> eval(l) + eval(r);
            case Mul(Expr l, Expr r) -> eval(l) * eval(r);
        };
    }

    public static void main(String[] args) {
        // 2 + 3 * 4
        System.out.println(eval(new Add(new Num(2), new Mul(new Num(3), new Num(4)))));
    }
}

実行結果は 14 でした。足し算・掛け算に加えて引き算を足したくなったら、permits に Sub を足します。すると eval の switch がコンパイルエラーになり、処理を書き足す場所がすぐにわかります。

sealedの使いどころ3パターン

パターン1:成功と不成立を型で表す

処理の結果を「成功のときの値」と「うまくいかなかったときの説明」のどちらかで返したいとき、sealed とジェネリクスを組み合わせます。

sealed interface Result<T> permits Success, Failure { }
record Success<T>(T value) implements Result<T> { }
record Failure<T>(String message) implements Result<T> { }

static Result<Integer> parse(String s) {
    try {
        return new Success<>(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return new Failure<>("数値に変換できません: " + s);
    }
}
for (String s : List.of("42", "abc")) {
    String msg = switch (parse(s)) {
        case Success<Integer>(Integer v) -> "OK: " + (v * 2);
        case Failure<Integer>(String m) -> "NG: " + m;
    };
    System.out.println(msg);
}

実行結果:

OK: 84
NG: 数値に変換できません: abc

呼び出し側は、成功と不成立の両方を書かないとコンパイルが通らないので、片方の扱いを書き忘れることがなくなります。

パターン2:enum と record を1つの型にまとめる

enum は暗黙的に final なので、sealed インターフェースの許可されたサブクラスにできます。決まった選択肢(enum)と、自由な値を持つ型(record)を1つにまとめられます。

sealed interface Label permits Size, Free { }
enum Size implements Label { S, M, L }
record Free(String text) implements Label { }

static String label(Label l) {
    return switch (l) {
        case Size.S -> "小";
        case Size.M -> "中";
        case Size.L -> "大";
        case Free(String text) -> text;
    };
}

label(Size.M) は 中、label(new Free("特注")) は 特注 を返しました。enum の定数を Size.M のように型名付きで case に書く形は、Java 21 で動かして確認した書き方です。enum の定数を1つ足すと、この switch もコンパイルエラーで知らせてくれます。

パターン3:enum と sealed の選び分け

表したいもの 合う書き方 理由
種類だけが決まっていて、種類ごとのデータは同じ形 enum 定数が1つずつで足りる。values() や EnumMap が使える
種類ごとに持つデータの形が違う sealed interface と record 型ごとに別のフィールドを持てる
種類ごとにインスタンスをいくつも作りたい sealed interface と record enum の定数は1つずつしか存在しない
一部の種類だけ利用者に拡張を開放したい sealed と non-sealed 枝ごとに開け閉めを決められる
だれでも自由に実装してよい 通常の interface 制限しないことが目的

enum でも、switch 式で定数を書き漏らすと the switch expression does not cover all possible input values という同じメッセージのコンパイルエラーになります(今回の実行で確認)。「網羅をコンパイラが確かめる」という考え方は enum と sealed で共通で、sealed はそれを「データの形が違う型」にまで広げたもの、と捉えると理解しやすくなります。

sealedクラスとキャストの関係

sealed で型の範囲が決まると、コンパイラは「ありえないキャスト」も見つけられるようになります。

sealed interface S permits A, B { }
record A() implements S { }
record B() implements S { }

static void g(S s) {
    Runnable r = (Runnable) s;
}
Err8.java:13: error: incompatible types: S cannot be converted to Runnable
        Runnable r = (Runnable) s;
                                ^

通常のインターフェースどうしなら、「両方を実装したクラスがどこかにあるかもしれない」ため、このキャストはコンパイルが通ります。sealed の場合は、S になれるのが A と B だけで、どちらも final(record)で Runnable を実装していないことがコンパイラにわかるため、コンパイルの段階で知らせてくれます。

つまずきやすい点のチェックリスト

確認すること 理由
許可されたサブクラスに final・sealed・non-sealed のどれかを書いた(record・enum は不要) 書かないと modifiers expected のエラーになる
サブクラスは親と同じパッケージ(モジュールを使うなら同じモジュール)にある 別の場所に置くとコンパイルエラーになる
switch に default を書いていない 書くと、型を足したときにコンパイラが知らせなくなる
具体的な型の case を上、広い型の case を下に書いた 逆にすると dominated のエラーになる
when 付きの case の後ろに、条件なしの case を置いた when 付きだけでは網羅にならない
abstract でない sealed クラスは、その型自身の case も書いた そのクラスのインスタンスも存在しうる
null がありうるなら case null を書いた 書かないと NullPointerException になる
使っている Java のバージョンが、switch の型パターンに対応している(21 以降) 17 では instanceof で分岐する

よくある質問

Q. sealed と final は何が違いますか? A. final は「だれも継承できない」、sealed は「permits に書いた型だけが継承できる」です。sealed は、継承を完全に閉じるのではなく、許可する相手を選んで開けるための仕組みです。許可されたサブクラスの側には、final・sealed・non-sealed のどれかを書いて、その先の継承をどうするかを決めます。

Q. permits は必ず書かないといけませんか? A. 親と許可されたサブクラスを同じファイルに書くなら省略できます。別のファイルに分けるときは、permits に型を並べます。別ファイルで省略すると、今回の再現では class is not allowed to extend sealed class などのコンパイルエラーになるので、メッセージを見て permits に足せば解決します。

Q. sealed interface を switch で分岐するのに、default は書かないほうがよいですか? A. 型ごとに処理を書き分けたいなら、書かないほうが利点が大きくなります。default がなければ、型を書き漏らしたときや、あとから permits に型を足したときに、コンパイルエラーで気づけます。 default を書くと、今回の実行結果のように、書いていない型が静かに default へ流れます。

Q. Java 17 でも sealed を使う意味はありますか? A. あります。継承できる型を限定できるので、「想定していない実装クラスが増える」ことを防げます。分岐は instanceof のパターンマッチングで書きます。Java 21 に上げると、同じ sealed の定義のままで、switch による網羅チェックと record パターンが使えるようになります。

まとめ

  • sealed は継承・実装できる型を限定し、permits に許可する型を並べる。 同じファイルに書くなら permits は省略できる。
  • 許可されたサブクラスには final・sealed・non-sealed のどれかを書く。 record と enum は暗黙的に final なので不要。
  • permits にない型が継承すると class is not allowed to extend sealed class のコンパイルエラーになる。
  • sealed な型を switch で分岐すると、分岐が足りないときに does not cover all possible input values で知らせてくれる。
  • default を書かないことで、型の追加にコンパイルエラーで気づける。 default を書くと書き漏れが実行時まで表に出ない。
  • sealed は Java 17 から、switch の型パターンと record パターンは Java 21 から。 Java 17 では instanceof で分岐する。
  • 種類ごとにデータの形が違うなら sealed と record、種類だけなら enum が合う。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java sealedクラスの使い方|permitsとswitchの書き方」

株式会社RJCでは、Javaのsealedクラスのような新しい文法を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?