【Java】あなたとJavaとEnum

お久しぶりです。

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

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

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

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

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

JavaEnumのしくみと使い方

JavaEnumJDKでもあんまり使われていません。色々な現場でも、ライブラリでも、使われているのを見ると「おっ、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で使用する際にNULLだとNullPointerExceptionが発生するからです。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3,
    UNKNOWN,
}

// nullのEnumをswitchのconditionに使用するとNullPointerExceptionが発生する
TestEnum en = null;

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の値を定義する

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

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書いてるし。

ちょっとエキセントリックなんですが、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なメソッドを呼ぶだけ、とかそんなことも出来ます。

巷でテンプレートパターンと呼ばれているものはすべて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, 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()メソッドを使うことで全列挙子を取得することが出来る
        for(TestEnum e : TestEnum.values()) {
            if(value == e.getValue()) return e;
        }
        
        // ここはthrow new IllegalArgumentExceptionでもいいと思う
        // (暗黙に定義されるvalueOf(String)だとIllegalArgumentExceptionを返す)
        return TestEnum.UNKNOWN;
    }
}

[2014/12/15追記:説明しようと思っていて忘れていたEnumMapとEnumSetについて追記]

Enumのためのコレクション

EnumMap

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

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

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

EnumSet

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

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

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

「従来の int ベースの「ビットフラグ」に対する高品質かつ型保証された代替として十分に使用可能です。」とは書いてあるものの、そもそもビット操作をしなきゃいけない時点でEnumなんて使わなそうだし、そもそもJavaを(省略)

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

Enumの実用例

仕組みと出来ることの紹介はこんなもんでいいでしょう。

次は実際にこんな風に使ったら中々便利だったよ、って言うのを少し紹介してみます。

Comparator<T>の実装

いま作っているものにこんなデータクラスがありまして。

@Data
public class Subject implements Serializable {
    private static final long serialVersionUID = -6818315400153409211L;
    private String author;
    private String title;
    private int logNo;
    private Date postDate;
    private Date editDate;
    private int commentCount;
    private int point;
    private double rate;
    private String[] tag;
    private long key;
    private double size;
    private int odai;
    private String[] characters;
    private boolean isFavorited;
}

この各プロパティでソートするためのComparator<T>Enumで実装してみました。

public enum SubjectSort implements Comparator<Subject> {
    NUM {
        @Override
        public int compare(Subject l, Subject r) {
            return ((int) (l.getKey() - r.getKey())) * reverse();
        }
    },
    TITLE {
        @Override
        public int compare(Subject l, Subject r) {
            return l.getTitle().compareTo(r.getTitle()) * reverse();
        }
    },
    AUTHOR {
        @Override
        public int compare(Subject l, Subject r) {
            return l.getAuthor().compareTo(r.getAuthor()) * reverse();

        }
    },
    CREATED {
        @Override
        public int compare(Subject l, Subject r) {
            return l.getPostDate().compareTo(r.getPostDate()) * reverse();
        }
    },
    LASTUP {
        @Override
        public int compare(Subject l, Subject r) {
            return l.getEditDate().compareTo(r.getEditDate()) * reverse();
        }
    },
    EVAL {
        @Override
        public int compare(Subject l, Subject r) {
            return l.getCommentCount() - r.getCommentCount() * reverse();
        }
    },
    POINT {
        @Override
        public int compare(Subject l, Subject r) {
            return l.getPoint() - r.getPoint() * reverse();
        }
    },
    RATE {
        @Override
        public int compare(Subject l, Subject r) {
            return ((int) (l.getRate() - r.getRate())) * reverse();
        }
    },
    SIZE {
        @Override
        public int compare(Subject l, Subject r) {
            return ((int) (l.getSize() - r.getSize())) * reverse();
        }
    };

    @Setter protected boolean isReverse = false;

    protected int reverse() {
        return !isReverse ? 1 : -1;
    }
    
    @SuppressLint("DefaultLocale")
    public static SubjectSort fromQuery(String query) {
        return SubjectSort.valueOf(query.toUpperCase());
    }
}

後はCollections#sortにこのEnumを渡すだけです。

結局何処かに各Comparatorは宣言と実装しなきゃいけないんですが、Enumでまとめることによって可読性をあげることが出来ます。

また、元々これはとあるWebアプリの機能をそのまま移植しようと思っているもので、もしintent-filterか何かでURIが渡されてきた時にソート用のクエリも渡されてきたら、そのクエリからEnumを生成できるようにしています。

WebAPIを叩く

WebAPIを叩く時にも便利です。

以前、YOのAPIを叩いた時はこんなEnumを作成しました。

public enum YoAPI {
    YO("http://api.justyo.co/yo/", Request.Method.POST),
    YO_ALL("http://api.justyo.co/yoall/", Request.Method.POST),
    ACCOUNTS("https://api.justyo.co/accounts/", Request.Method.POST),
    SUBSCRIBERS_COUNT("https://api.justyo.co/subscribers_count/", Request.Method.GET);

    @Getter private final String value;
    @Getter private final int method;

    private YoAPI(String value, int method) {
        this.value = value;
        this.method = method;
    }
}

後はVolleyのRequestにこのEnumを渡すだけ…ではどうにもなりませんが、エンドポイントやGET / POSTの区別なんかを書いておく分には十分すぎますし、固定で渡さなきゃいけないようなquery / paramsがあるなら持たせておいてもいいでしょう。

[2014/12/25追記]AndroidのArrayAdapterで使う

AndroidだとSpinnerで使うと超絶便利です。

こんなArrayAdapterを作っておきます。ジェネリクス複数制限なんて生まれて始めて使いました。

// 2014/01/04 ジェネリクスの制限かける場所が間違っていたので修正
public class EnumSpinnerAdapter<T extends Enum<T> & AdaptableEnum> extends ArrayAdapter<T> {

    public static interface AdaptableEnum {
        String getDescription();
    }

    private final LayoutInflater _layoutInflater;

    public EnumSpinnerAdapter(Context context, Set<T> enumSet) {
        super(context, 0, new ArrayList<T>(enumSet));
        _layoutInflater = LayoutInflater.from(context);
    }

    public EnumSpinnerAdapter(Context context, T[] enums) {
        super(context, 0, enums);
        _layoutInflater = LayoutInflater.from(context);
    }

    static final EnumSpinnerViewHolder {
        final TextView description;

        EnumSpinnerViewHolder(View v) {
            this.description = (TextView) v.findViewById(R.id.txvDescription);
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        EnumSpinnerViewHolder vh = null;
        View view = convertView;

        if (view == null) {
            view = _layoutInflater.inflate(R.layout.adapter_enum_spinner, null);
            vh = new EnumSpinnerViewHolder(view);
            view.setTag(vh);
        } else {
            vh = (EnumSpinnerViewHolder) view.getTag();
        }

        val item = this.getItem(position);

        vh.description.setText(item.getDescription());

        return view;
    }
}

Enumはこんな感じ。

public enum UgigiMode implements AdaptableEnum {
    ALL("全体検索", "free"),
    TITLE("タイトル検索", "title"),
    AUTHOR("作者検索", "author"),
    TAG("タグ検索", "TAG");

    @Getter private final String description;
    @Getter private final String value;

    private UgigiMode(String description, String value) {
        this.description = description;
        this.value = value;
    }
}

Spinnerに値をセットするのはこれだけ。

val spinner = (Spinner) findViewById(R.id.spiMode);
spinner.setAdapter(new EnumSpinnerAdapter<UgigiMode>(getApplicationContext(), UgigiMode.values()));

わざわざSet<T>を受け取れるようにもしてあるので、EnumSetのstaticメソッドを使ってもOK。

値の取得もこれだけ。

val spinner = (Spinner) findViewById(R.id.spiMode);
val mode = (UgigiMode) spinner.getSelectedItem();

後はswitchで条件分岐してもいいし、Enumに自分で定義したメソッドを呼び出してもいいし、Intent#putExtraでそのまま別画面に渡してもOK。本当にめちゃめちゃ便利です。

まとめ

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

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

参考