【Java】あなたと Java と Enum

2019/01/02 追記

  • シンタックスハイライトを適用しました。
  • 大幅な加筆を行いました。
    • いくつかのサンプルコードを Java8 以降の内容に書き直しました。

前書き

お久しぶりです。

お久しぶりですと言っても、定期購読している人は恐らく数人しかいないんじゃないでしょうか。技術ブログなんてそんなものです。

別に忙しかったわけじゃないんですが…いや…忙しかったのかな。忙しかったんですが色々遊んでもいました。単にネタにするものがなかっただけです。

最近は 2〜3 年ぐらい前に作成して 1 年間ほったらかしにしていた Android アプリの改修をゴリゴリやっています。

とりあえず機能追加は一切しないでリファクタリングと簡単な障害対応メインで…と思ったらあまりのクソコードさに失禁しかけたので、ほとんど全コード書き直しています。しかし、2〜3 年前の自分を殺したくなるのは、エンジニアにとってはいい兆候です。(ポジティブ)

さて、今回はAndroid のパフォーマンスに関する手引きで「使ったらぶっ殺すからな」と言われていた Enum に関する記述がいつの間にか消えていたので、Java での Enum に関するあれこれを書いていきます。

JavaEnum のしくみと使い方

JavaEnumJDK でもあんまり使われていません。まぁこれは Java 1.5 からの機能なので、互換性的な問題があるからかもしれません。

しかし、色々な現場でも、ライブラリでも、使われているのを見ると「おっ、Enum が使われているぞ!?」って気分になります。C#なんかはどこでもバリバリ使っていて非常に楽なんですが。

Java であんまり使われない理由、そして Android で忌避されていた一番大きな理由は、あまりにもリッチすぎるからだと思われます。

とりあえず Enum の宣言方法の紹介も兼ねて適当なコードを載せておきましょう。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3, //最後がカンマで終わっていてもOK
}

基本的にはこんな感じです。応用すると色々なことが出来すぎてしまうんですが、これは後で説明します。

上記のコードをコンパイルすると内部でこんなコードに変換されます。

final class TestEnum extends Enum<TestEnum> {
    private TestEnum(String name, int ordinal) {
        super(name, ordinal);
    }

    // コンストラクタのnameには列挙子の名前、ordinalには宣言された順番が入る
    public static final TestEnum TEST1 = new TestEnum("TEST1", 0);
    public static final TestEnum TEST2 = new TestEnum("TEST2", 1);
    public static final TestEnum TEST3 = new TestEnum("TEST3", 2);

    private static final TestEnum ENUM$VALUES[] = { TEST1, TEST2, TEST3 };

    // 他にもvalueOf(String name)やvalues()のようなstaticメソッドが生成されるが割愛

}

と言うわけで、Enum を宣言すると中の列挙子の分だけインスタンスが作成されます。static finalなので==での比較が可能になるわけです。

TestEnum en = TestEnum.TEST1;
if (en == TestEnum.TEST1) {
    // もちろんen.equals(TestEnum.TEST1)でもtrueを返す
}

Enum もオブジェクトなので宣言時にnullを入れておくことが出来ますが、なるべく初期値用の列挙子を用意しておくべきです。

と言うのも、Enumswitchの condition として指定した変数がnullだとNullPointerExceptionが発生するからです。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3,
    UNKNOWN,
}
// Enum に null を指定
TestEnum en = null;

// switch の condition に使用すると NullPointerException が発生する
switch(en) {
case TEST1 :
break;
case TEST2 :
break;
case TEST3 :
break;
default :
break;
}
// そこで、初期値用の列挙子「UNKNOWN」を用意しておく
TestEnum en = TestEnum.UNKNOWN;

switch(en) {
case TEST1 :
break;
case TEST2 :
break;
case TEST3 :
break;
case UNKNOWN : // 初期値用の列挙子の場合は default にフォールスルーしておけばよい
default :
break;
}

そもそも何でswitchNullPointerExceptionが発生するかなんですが、内部で Enum#ordinal()を呼び出しているからです。

また、上記の記事を読めばわかる通り、各ラベル内で自動で try-catch を行います。そのため、Enum の switch は普通の switch よりもコストが高い、と言うことは覚えておきましょう。

Enum の値を定義する

いきなりデメリットばかり取り扱ったせいでこんなクソ機能いらないでしょ、と思うかもしれませんが、そんなことはありません。色々工夫すると非常に強力な機能に化けます。

JavaEnum は各言語の Enum 同様、列挙子に値を定義することが出来ます。ちょっと変わった形ですが、こんな感じです。

public enum TestEnum {
    // 値を定義する場合は最後がセミコロンで終わってないといけない
    TEST1(0),
    TEST2(1),
    TEST3(2);

    private final int value;

    // privateスコープでコンストラクタを定義する
    // (* public, protected, package-privateではダメ。
    //    新しいインスタンスを外部から勝手に生成されたらEnumのメリットがなくなるので…。)
    private TestEnum(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }
}

先ほどの話のおさらいになりますが、結局のところ Enum の列挙子はクラスのインスタンスです。だから、コンストラクタで初期値を渡してもそれは Java の仕様内の話になるので合法です。

初期値の型に制限はありませんし、コンストラクタに渡す引数の数にも制限はありませんし、コンストラクタのオーバーロードがいくつあろうと構いません。だってそれは Java の仕様内の話だから。

値を返すメソッド etc や値を保持しておくフィールドは自分で定義しなきゃいけませんが、まぁ、そんなのLombok使っておけばいいじゃないですか。

また、EnumSerializableを実装しているので、保持する値もSerializableであれば、そのままシリアライズ / デシリアライズが可能です。

今回の例ではvalueを final にしたり、setter を用意したりしませんでしたが、どちらも制限があるわけではないので、Enum なのに何らかのステートを持たせるなんてことも可能です。可能ですが、それはもう一般的な Enum からは逸脱しすぎている気がします。

Enum にメソッドを定義する

何度でも言いますが、Enum の列挙子はクラスのインスタンスです。クラスに対しメソッドを定義しても誰にも怒られません。って言うかさっきの例で思いっきり getter 書いてるし。

ちょっとエキセントリックなんですが、Enumabstractメソッドを定義することも出来ます。じゃあそのabstractなメソッドはどこで実装するの?と言うと、宣言時です。

public enum TestEnum {
    TEST1 {
        @Override
        public String test() {
            return "TEST1";
        }
    },
    TEST2 {
        @Override
        public String test() {
            return "TEST2";
        }
    },
    TEST3 {
        @Override
        public String test() {
            return "TEST3";
        }
    };

    public abstract String test();
}

いわゆる匿名(無名)クラス的な扱いになるわけですね。この機能をフル活用すると、Enum なのにswitchで分岐せずabstractなメソッドを呼ぶだけ、とかそんなことも出来ます。*1

巷でテンプレートパターンと呼ばれているものはすべて Enum で代替可能です。代替すべきかどうかはおいといて。

また、メソッドが定義できると言うことは、インターフェースを実装することも可能だと言うことになります。Enum なのに別インターフェースとしても振舞えるわけです。気持ち悪いですね。

ただし、他のクラスを継承することはできません。なぜなら、すべての Enum は暗黙的にEnum<E extends Enum<E>>を継承しているからです。二重継承になってしまいます。

文字列と Enum の相互変換

個人的に Enum の一番の強みはここだと思っています。文字列 ⇔ Enum への変換がめちゃくちゃ楽です。

特に JSONXML をパースする時に恐ろしい力を発揮します。

文字列 → Enum

文字列から Enum へ変換するにはvalueOfメソッドを使用します。これには javaDoc がありません。なぜなら、コンパイル時に自動生成されるstaticメソッドだからです。

// TestEnum.TEST1を文字列から生成する
TestEnum en = TestEnum.valueOf("TEST1");

Enum#valueOf(Class<T>, String)なんてものもありますが、リフレクションでもしない限りは使わないと思います。

Enum → 文字列

これには二通りの方法があります。Enum#name()を使用する方法と、Enum#toString()を使用する方法です。

違いは非常に簡単で、#name()はオーバーライド不可、#toString()はオーバーライド可能なメソッドです。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3 {
        @Override
        public String toString() {
            return "TEST3だよ";
        }
    };
}
System.out.println(TestEnum.TEST1.name())
// => TEST1
System.out.println(TestEnum.TEST2.name())
// => TEST2
System.out.println(TestEnum.TEST3.name())
// => TEST3

System.out.println(TestEnum.TEST1.toString())
// => TEST1
System.out.println(TestEnum.TEST2.toString())
// => TEST2
System.out.println(TestEnum.TEST3.toString())
// => TEST3 だよ

いろんな値 ⇔ Enum

自分でメソッドを作ればいいじゃない。(完)

public enum TestEnum {
    TEST1(0),
    TEST2(1),
    TEST3(2),
    UNKNOWN(-1);

    private final int value;
    private TestEnum(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    public static TestEnum valueOf(int value) {
        // 暗黙的に定義されるvalues()メソッドを使うことで全列挙子を取得することが出来る
        return Arrays.stream(TestEnum.values())
            .filter(e -> value == e.getValue())
            .findFirst()
            .orElse(TestEnum.UNKNOWN);
    }
}

Enum のためのコレクション

EnumMap

Enum には専用のMap実装が用意されています。その名もEnumMapです。そのまんまじゃないか。

使い方はHashMapとほとんど変わりません。ただ、Key の比較にEnum#ordinalを使用しているので、HashMapより速いです。

しかしまぁ、Enum 側でいくらでも紐付くプロパティを定義出来るので、実はそんなに使う機会がないと言う…。存在自体を忘れないようにしたいですね。(自戒)

EnumSet

EnumSetなるものも用意されています。

これはstaticメソッドからのみインスタンスを生成することが出来ます。よく使いそうなものだけ挙げておきます。

そもそもある Enum の全列挙子を取得したいのであればEnum#values()を使用すればいいのですが、Enum を使ってビット操作のようなことがしたい場合はこれを使うといいよ、と言われています。

ドキュメントには

従来の int ベースの「ビットフラグ」に対する高品質かつ型保証された代替として十分に使用可能です。

と書いてあるものの、そもそもビット操作をしなきゃいけない時点で Enum なんて使わなそうだし、そもそも Java を(省略)

具体的にどうやってビット操作の真似事をするかと言うと、Setadd / removeを使ってフラグの上げ下げをするだけです。なんだそれ。

Enum 共通のdefault / staticメソッドを定義する

コード値との相互変換

DB のカラムに格納する値だとか、Web なら<select>で使うような値を Enum にしておくと色々捗ります。

少なくとも変換用のマジックナンバーを用意したり、constant な値だけを定義したクラス or インターフェースを用意するよりよっぽど簡単かつ便利です。自然とモジューラブルになるので、保守性も高いです。

(色々面倒なのでLombok使用前提で…。)

/**
 * 性別
 */
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public enum Gender {
    MALE("男", "1"),
    FEMALE("女", "2"),
    OTHER("その他", "3"),
    UNKNOWN("", "9");

    private final String label;
    private final String code;

    public static Gender ofCode(String code) {
        return Arrays.stream(Gender.values())
            .filter(e -> e.getCode().equals(code))
            .findFirst()
            .orElse(Gender.UNKNOWN);
    }
}

で、実際に作って行くと、ofCodeメソッドを共通化したくなってくることがあると思います。

先述した通り Enum は暗黙的にEnum<E extends Enum<E>>を継承しているため、共通処理を定義する場合はインターフェースを作るしかありません。

Enum 共通処理を定義したインターフェース

幸い Java8 からはインターフェースにdefault / staticメソッドを定義できるので、クラスを継承できなくても何とかなります。

public interface CodableEnum<E extends Enum<E>> {
    String getCode();

    @SuppressWarnings("unchecked")
    static <E extends Enum<E>> CodableEnum<E> ofCode(String code, CodableEnum<E> orElse) {
        return Arrays.stream(orElse.getClass().getEnumConstants())
                .filter(e -> e.getCode().equals(code))
                .map(e -> (CodableEnum<E>) e)
                .findFirst()
                .orElse(orElse);
    }
}

ちょっと面倒なのはEnum#values()が使えないことでしょうか。代わりにClass#getEnumConstants()を利用して Enum の一覧を取得する必要があります。

後は先程作った Enum に実装してやれば OK です。

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public enum Gender implements CodableEnum<Gender> {
    MALE("男", "1"),
    FEMALE("女", "2"),
    OTHER("その他", "3"),
    UNKNOWN("", "9");

    private final String label;
    private final String code;
}
final Gender gender = Gender.ofCode("1", Gender.UNKNOWN);
// -> Gender.MALE が返される

まとめ

とまぁ、アイデア次第で色々と出来てしまいます。

public static finalなフィールドが多すぎてやってられなくなったりしてきたら、Enum を使って大富豪的プログラミングに興じてみるのはいかがでしょうか。

参考

*1:Template パターンのようなものだと考えれば良い。