【Java】あなたと Java と Enum
2019/01/02 追記
- シンタックスハイライトを適用しました。
- 大幅な加筆を行いました。
- いくつかのサンプルコードを Java8 以降の内容に書き直しました。
前書き
お久しぶりです。
お久しぶりですと言っても、定期購読している人は恐らく数人しかいないんじゃないでしょうか。技術ブログなんてそんなものです。
別に忙しかったわけじゃないんですが…いや…忙しかったのかな。忙しかったんですが色々遊んでもいました。単にネタにするものがなかっただけです。
最近は 2〜3 年ぐらい前に作成して 1 年間ほったらかしにしていた Android アプリの改修をゴリゴリやっています。
とりあえず機能追加は一切しないでリファクタリングと簡単な障害対応メインで…と思ったらあまりのクソコードさに失禁しかけたので、ほとんど全コード書き直しています。しかし、2〜3 年前の自分を殺したくなるのは、エンジニアにとってはいい兆候です。(ポジティブ)
さて、今回はAndroid のパフォーマンスに関する手引きで「使ったらぶっ殺すからな」と言われていた Enum に関する記述がいつの間にか消えていたので、Java での Enum に関するあれこれを書いていきます。
Java の Enum のしくみと使い方
Java の Enum は JDK でもあんまり使われていません。まぁこれは 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
を入れておくことが出来ますが、なるべく初期値用の列挙子を用意しておくべきです。
と言うのも、Enum をswitch
の 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; }
そもそも何でswitch
でNullPointerException
が発生するかなんですが、内部で Enum#ordinal()
を呼び出しているからです。
また、上記の記事を読めばわかる通り、各ラベル内で自動で try-catch を行います。そのため、Enum の switch は普通の switch よりもコストが高い、と言うことは覚えておきましょう。
Enum の値を定義する
いきなりデメリットばかり取り扱ったせいでこんなクソ機能いらないでしょ、と思うかもしれませんが、そんなことはありません。色々工夫すると非常に強力な機能に化けます。
Java の Enum は各言語の 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使っておけばいいじゃないですか。
また、Enum はSerializable
を実装しているので、保持する値もSerializable
であれば、そのままシリアライズ / デシリアライズが可能です。
今回の例ではvalue
を final にしたり、setter を用意したりしませんでしたが、どちらも制限があるわけではないので、Enum なのに何らかのステートを持たせるなんてことも可能です。可能ですが、それはもう一般的な Enum からは逸脱しすぎている気がします。
Enum にメソッドを定義する
何度でも言いますが、Enum の列挙子はクラスのインスタンスです。クラスに対しメソッドを定義しても誰にも怒られません。って言うかさっきの例で思いっきり getter 書いてるし。
ちょっとエキセントリックなんですが、Enum にabstract
メソッドを定義することも出来ます。じゃあその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 への変換がめちゃくちゃ楽です。
特に JSON や XML をパースする時に恐ろしい力を発揮します。
文字列 → 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 を(省略)
具体的にどうやってビット操作の真似事をするかと言うと、Set
のadd
/ 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 を使って大富豪的プログラミングに興じてみるのはいかがでしょうか。
参考
- Java 列挙型メモ(Hishidama’s Java enum Memo)
- J2SE 5.0 Tiger 虎の穴 Typesafe Enum
- Java の enum は継承できないけどインタフェースが継承できる - No Programming, No Life
*1:Template パターンのようなものだと考えれば良い。