Java の enum

イマドキの Java には enum があるんですよ実は、という話。
知ってるよそんなこと!な人は読むまでもないかも。

enum って?

列挙型のこと。C とか C++ とか C# とかでおなじみのアレ。
単純な enum は Java でもこれらの言語の enum と同じような記述になるけど、これらの言語の enum が整数型をベースにしているのに対して、Java ではオブジェクトをベースにしている点が異なる。
まぁその話は後ほど・・・

単純な enum

ただ列挙するだけの enum なら、本当に C や C++ や C# とほとんど変わらない。

// 信号機の色
enum SignalColor { RED, BLUE, YELLOW }

ただこれだけ。末尾には、余分なカンマがあってもいい。

enum SignalColor { RED, BLUE, YELLOW, }

更に、末尾にセミコロンも記述できる。セミコロンの意味については後ほど・・・

enum SignalColor { RED, BLUE, YELLOW; }

あわせ技。

enum SignalColor { RED, BLUE, YELLOW, ; }


で、これを使うときは、こんな感じ。

static String colorName(SignalColor color) {
    switch (color) {
    case RED: return "赤";
    case BLUE: return "青";
    case YELLOW: return "黄";
    default: throw new IllegalArgumentException();
    }
}

public static void main(String[] args) {
	System.out.println(colorName(SignalColor.BLUE));
}

ここで、case ラベルの指定で型名である SignalColor が不要な点に注意。
switch 文の case ラベル以外の部分、例えば main メソッドでは型名も記述する必要がある。
また、colorName メソッドに null を渡すと、予想に反して NullPointerException が発生する。
なんで IllegalArgumentException じゃなくて NullPointerException になるかは、各自 jad するなり javap するなりして確かめること!

フィールドを持った enum

Java の enum はフィールドを持つこともできる。

enum SignalColor {
    RED, BLUE, YELLOW;
    String colorName;
}

このように、セミコロンの後にフィールドを宣言する。

static String colorName(SignalColor color) {
    return color.colorName;
}

public static void main(String[] args) {
    SignalColor.RED.colorName = "赤";
    SignalColor.BLUE.colorName = "青";
    SignalColor.YELLOW.colorName = "黄";
    System.out.println(colorName(SignalColor.BLUE));
}

また、通常はイミュータブルになるように、

enum SignalColor {
    RED("赤"),
    BLUE("青"),
    YELLOW("黄");
    public final String colorName;
    private SignalColor(String colorName) {
        this.colorName = colorName;
    }
}

のように記述する。
このように、独自のコンストラクタを記述した場合、その呼び出しは列挙子に小括弧を記述して、その中に引数を指定する*1。
また、コンストラクタのアクセス修飾子には public と protected は指定することが出来ない上、指定しなかった場合でも package private ではなく、private とみなされる。
コンストラクタは複数用意することもでき、列挙子ごとに違うコンストラクタを呼び出すことも可能。


使う側は、

static String colorName(SignalColor color) {
    return color.colorName;
}

public static void main(String[] args) {
    System.out.println(colorName(SignalColor.BLUE));
}

こう。


フィールドが持てるのは、Java の enum が実はタイプセーフ enum というイディオムのシンタックスシュガーとなっているためで、例えば上の例は

class SignalColor {
    private final String colorName;
    private SignalColor(String colorName) { this.colorName = colorName; }
    
    public static final SignalColor RED = new SignalColor("赤");
    public static final SignalColor BLUE = new SignalColor("青");
    public static final SignalColor YELLOW = new SignalColor("黄");
}

とほぼ等価*2。

メソッドを持った enum

Java の enum は、フィールドだけにとどまらず、メソッドを記述することもできる。

enum SignalColor {
    RED, BLUE, YELLOW;
    String colorName() {
        switch (this) {
        case RED: return "赤";
        case BLUE: return "青";
        case YELLOW: return "黄";
        default: throw new IllegalStateException();
        }
    }
}
public static void main(String[] args) {
    System.out.println(SignalColor.BLUE.colorName());
}

一番最初に示した、enum の外側で switch するコードと比べると、colorName メソッド内で NullPointerException が発生しないと言う利点を持つ (Java では this は null にならない)。


また、フィールドとあわせて、

enum SignalColor {
    RED("赤"),
    BLUE("青"),
    YELLOW("黄");
    private final String colorName;
    SignalColor(String colorName) {
        this.colorName = colorName;
    }
    String colorName() { return colorName; }
}

とすることで、列挙子を増やした場合に、修正が必要な箇所がなくなる (一箇所追加するだけでいい)。

列挙子ごとにオーバーライド

列挙子ごとにメソッドをオーバーライドすることもできる。

enum SignalColor {
    RED {
        @Override
        String colorName() { return "赤"; }
    },
    BLUE {
        @Override
        String colorName() { return "青"; }
    },
    YELLOW {
        @Override
        String colorName() { return "黄"; }
    };
    String colorName() { throw new UnsupportedOperationException(); }
}
抽象メソッドを持った enum

上の例だと、抽象メソッドにすることでコンパイルチェックすることができる。

enum SignalColor {
    RED {
        @Override
        String colorName() { return "赤"; }
    },
    BLUE {
        @Override
        String colorName() { return "青"; }
    },
    YELLOW {
        @Override
        String colorName() { return "黄"; }
    };
    abstract String colorName();
}

また、インターフェイスを実装することもできるため、

interface Color { String colorName(); }

enum SignalColor implements Color {
    RED {
        public String colorName() { return "赤"; }
    },
    BLUE {
        public String colorName() { return "青"; }
    },
    YELLOW {
        public String colorName() { return "黄"; }
    }
}

のように、任意のインターフェイスを実装できる。

クラス内に記述する enum について

あるクラスに enum を含めた場合、static を付けても付けなくても static とみなされる。
なので、外側の this を使うことは出来ない。

enum で使用できるメソッドの便利な使い方

values()

Enum クラスに定義されていないので見落としがちだが、各 enum には static メソッドとして values メソッドが定義される。
このメソッドは、全ての列挙子を含む配列を記述された順番で返すので、数値から列挙子オブジェクトへの変換に使用できる。

static SignalColor valueOf(int i) {
    return SignalColor.values()[i];
}

public static void main(String[] args) {
    SignalColor c = valueOf(1);
    System.out.println(c.name());   // BLUE
}
valueOf(String name)

これも Enum クラスに定義されていないが、各 enum には static メソッドとして文字列のみを引数に取る valueOf メソッドが定義される。
このメソッドは、指定された名前を持つ列挙子オブジェクトを返す。

public static void main(String[] args) {
    SignalColor c = SignalColor.valueOf("BLUE");
    System.out.println(c.name());   // BLUE
}

何か忘れてない・・・?

C とかの enum は基本が数値型なので、フラグとして使用できる。
例えば、

#include <stdio.h>

typedef enum hoge {
    HOGE_1 = 1,
    HOGE_2 = 2,
    HOGE_3 = 4,
    HOGE_4 = 8
} hoge;

void f(hoge h)
{
    if (h & HOGE_2) puts("hoge 2");
    if (h & HOGE_3) puts("hoge 3");
}

int main()
{
    f(HOGE_3);
    f(HOGE_1 | HOGE_3);
    return 0;
}

こんな感じに 2 のべき乗を値として指定しておくことで、複数のフラグを管理できる。
これを Java の enum で実現しようとする場合、EnumSet を使用する。

enum Hoge {
    HOGE_1, HOGE_2, HOGE_3, HOGE_4
}

static void f(EnumSet<Hoge> h) {
    if (h.contains(Hoge.HOGE_2)) System.out.println("hoge 2");
    if (h.contains(Hoge.HOGE_3)) System.out.println("hoge 3");
}

public static void main(String[] args) {
    f(EnumSet.of(HOGE_3));
    f(EnumSet.of(HOGE_1, HOGE_3));
}

Java の enum 便利ですね!あとは null が排除できれば・・・!

*1:なので、今までの例でも中が空の小括弧を記述することが可能

*2:実際には Enum クラスを継承したりしているので、enum を使った方が色々できる