enumの使い方のおさらいと高機能enumとしてのSealedクラス

Javaでは複数の定数をまとめて扱う型としてenum(列挙型)が用意されています。
これはこれで便利なのですが物足りないところもあって、それがSealedクラスなどを使うことで解決できるようになるので、解説します。

enum

enumは状態やデータ区分を表すのによく使われます。
構文は次のようになります。

enum 型名 {
  列挙1, 列挙2, ...
}

例えば次のような状態を表すとします。

この状態を表すenumは次のようになります。それぞれの値は大文字で書くようにします。

enum State {
  READY, RUNNING, SUSPENDED, TERMINATED
}

enumではそれぞれの値ごとに処理を行うということがよくあります。そこでswitchと相性がいいです。

State s = State.READY;

switch (s) {
  case READY -> System.out.println("準備完了");
  case RUNNING -> System.out.println("実行中");
  case SUSPENDED-> System.out.println("停止中");
  case TERMINATED-> System.out.println("終了");
}

switch文の場合はenum値が足りなくても大丈夫ですが、switch式では全てのenum値を処理しないとエラーになります。

このことを考えると、enum値すべての処理をするswitchは式にしておいたほうがいいですね。(Java 14以降)

var message = switch (current) {
    case READY -> "準備完了";
    case RUNNING -> "実行中";
    case SUSPENDED -> "停止中";
    case TERMINATED -> "終了";
};

enumに値を持たせる

enum値それぞれに数値などを割り当てたい場合があります。そういう場合はフィールドとして定義することができます。 メソッドも定義できるのでgetterを用意するか、toStringメソッドをオーバーライドすることになりますね。 フィールドはprivate finalにしておきましょう。

enum State {
    READY("準備完了"),
    RUNNING("実行中"),
    SUSPENDED("停止中"),
    TERMINATED("終了");

    private final String name;
    State(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    @Override
    public String toString() {
        return name;
    }
}

ただまあ、フィールドとコンストラクタとgetterを定義していくのめんどいので、Lombokを使いたくなります。

@Value
enum State {
    READY(1, "準備完了"),
    RUNNING(2, "実行中"),
    SUSPENDED(3, "停止中"),
    TERMINATED(4, "終了");

    int code;
    String name;

    @Override
    public String toString() {
        return name;
    }
}

Sealedクラスを拡張enumとして使う

JavaenumはCのenumよりは便利なのですが、パラメータを追加したいという使い方ができません。
例えばMouseEventというenumがあって、CLICKEDとMOVEDがあるんだけど座標も持たせたい、というような場合です。あとCLICKEDにはどのボタンかも持たせたい。

という場合にSealedクラスが使えます。実際に使うのはinterfaceになるけど。

enum Button {
    LEFT, MIDDLE, RIGHT
}
sealed interface Event {
    record Clicked(Button button, int x, int y) implements Event {}
    record Moved(int x, int y) implements Event {}
}

sealedをつけたクラスやインタフェースでは、そのnestedクラスだけで継承が行えるようになります。nestedクラス以外で継承を行う場合にはpermitsで指定します。

sealed interface Event permits Clicked, Moved {}
record Clicked(Button button, int x, int y) implements Event {}
record Moved(int x, int y) implements Event {}

そして、Java 19でプレビューとして導入されたpattern matching for switchとrecord patternを利用すれば、enumのように扱えます。

Event e = new Event.Clicked(Button.RIGHT, 7, 3);
var message = switch (e) {
    case Event.Clicked(var b, int x, int y) -> 
            "%sボタンが%d,%dでクリックされた".formatted(b, x, y);
    case Event.Moved(int x, int y) -> 
            "%d,%dに動いた".formatted(x, y);
};

ここでEvent型はSealedなので、継承するクラスはClickedかMovedに限られるため、可能なすべての型をチェックしたことになります。

ということで、recordとsealedクラスとパターンマッチとswitch式を組み合わせると高機能enumのように使えるという話でした。