読者です 読者をやめる 読者になる 読者になる

Java の enum

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

enum って?

列挙型のこと。C とか C++ とか C# とかでおなじみのアレ。
単純な enumJava でもこれらの言語の 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

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

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));
}

こう。


フィールドが持てるのは、Javaenum が実はタイプセーフ 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

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

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 のべき乗を値として指定しておくことで、複数のフラグを管理できる。
これを Javaenum で実現しようとする場合、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));
}

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

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

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