Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

Typesafe Enum

 
 
Tiger Enum がないとは
 
 

Java で C/C++ の enum 型がないと嘆いている方は多いのではないでしょうか。

単に enum を導入するのは私は反対でした。というのも、enum 型には潜在的な問題があると思うからです。enum 型は結局 int 型と同じに扱われてしまうので、範囲のチェックなどやってくれません。たとえば

    typedef enum {ALPHA, BETA, GAMMA, DELTA} alpha;
    
    alpha a = ALPHA; // OK
    alpha b = 1;     // OK だけども、BETA のことだと誰が分かる
    alhpa g = 7;     // OK だけども、本当は定義していない

また、enum をごっちゃにしても言語仕様的には大丈夫なのです。

    typedef enum {SOUSEKI = 1000,
                  SHIKIBU = 2000,
                  INAZOU = 5000,
                  YUKICHI = 10000} yen;
    typedef enum {PENNY = 1, NICKEL = 5, DIME = 10, QUARTER = 25} cent;
 
    yen y = SOUSEKI - PENNY; // OK なんだけれど.....

このままだと安全が言語仕様に取り込まれている Java には似合わないと思うのです。

でも、結局 enum の代わりに static なクラス変数を使用していたら同じ問題が起きてしまいます。

    final static int ALPHA = 0;
    final static int BETA = 1;
    final static int GAMMA = 2;
    final static int DELTA = 3;
 
    int alpha = ALPHA; // OK
    int beta = 1;      // OK だけど、本当に BETA かどうか分かる ?
    int gamma = 10;    // 文法的には OK なんだけど、本当にあっている ?

このような問題がない enum なんてあるのでしょうか。あるのです。

それがタイプセーフ Enum です。

 

 
 
Tiger Effective Java を読んでみよう
 
 

タイプセーフ Enum は実をいうと Joshua Bloch さんが Effective Java という本の中で提案されているものなんです。ちなみに Joshua Bloch さんは JSR-201 の Spec Lead でもあります。

Effective Java の中ではタイプセーフ Enum はどのように説明されているのか簡単に紹介します。

タイプセーフ Enum の中心的なアイディアは定数を表わすためのクラスを使って、各定数はそのオブジェクトにしてしまうというのものです。ただ、勝手に new されてしまっても困るので、コンストラクタは private にしてしまいます。

Effective Java の中で示されている例は次のようなものです。

public class Suit {
    private final String name;
 
    private Suit(String name) {
       this.name = name;
    }
 
    public String toString() {
        return name;
    }
public static final Suit CLUBS = new Suit("clubs"); public static final Suit DIAMONDS = new Suit("diamonds"); public static final Suit HEARTS = new Suit("hearts"); public static final Suit SPADES = new Suit("spades"); }

これで完璧かと思うとまだまだなのです。このタイプセーフ Enum をシリアライズするにはどうすればいいかとか、振る舞いを持たせるにはどうすればいいかなどの議論が続きます。

完璧なタイプセーフ Enum を作るにはかなり面倒なのです。

しかし、Tiger でタイプセーフ Enum が導入されたことでそんなことを気にせずに enum を使用することができるようになったのです。

上の Suit クラスを enum で表わすと次のようになります。

    public enum Suit {CLUBS, DIAMONDS, HEARTS, SPADES}

enum というのが新しい予約語です。この定義をすれば Suit を型のように使うことができます。そして、enum の要素は頭に Suit をつけて、Suit.CLUBS などのように表わすことができます。

さっそく使ってみましょう。

public class EnumTest {
    enum Suit {CLUBS, DIAMONDS, HEARTS, SPADES}
 
    public static void main(String[] args) {
        Suit suit = Suit.CLUBS;
        System.out.println("Suit is " + suit);
    }
}

これを実行すると

C:\JSR201\examples>java EnumTest
Suit is CLUBS
 
C:\JSR201\examples>

なにも設定していないのに、出力すると CLUBS となります。普通の整数の定数だと、その値が出力されてしまい、それが実際にどれに対応するか調べる必要があったのですが、これだととても楽になります。

 

 
 
Tiger いろいろやってみよう
 
 

正しい enum の書き方をいちおう示しておきましょう。まずは基本的な書き方。

    ClassModifier enum Identifier { Arg1, Arg2, Arg3, ..., ArgN }

ClassModifier は public や static、final などです。これは省略可能です。

Enum にしたい定数は "," でつなげて記述します。たとえばこんな感じです。

    enum Alpha {a, b, c, d}

さっそく書いてみましょう。

サンプルのソース EnumTest1.java

まずは定義です。

public class EnumTest1 {
    enum Suit {CLUBS, DIAMONDS, HEARTS, SPADES}

代入

定義ができたところで、代入してみましょう。Enum は一種の型のように扱うことができるので、次のように書くことができます。

    public static void main(String[] args) {
        Suit suit = Suit.CLUBS;
        Suit suit2 = Suit.DIAMONDS;
 
        // 代入
        suit = suit2;
        suit2 = Suit.SPADES;

当たり前ですが、できます ^^;; これも当たり前ですが、C/C++ の enum と違って int に代入することはできません。コンパイルエラーになります。

 

比較

代入ができたので、つづいて比較してみましょう。まずは == が使えるかどうかを試してみます。

        // 比較 ==
        if (suit == Suit.HEARTS) {
            System.out.println("Suit is HEARTS");
        } else {
            System.out.println("Suit is NOT HEARTS");
        }

この部分の実行結果は

C:\JSR201\examples>java EnumTest1
Suit is NOT HEARTS

となります。== は使えることは予想通りですが、equals は使えるか試してみましょう。ついでに、compareTo も試してみます。

        // equals
        if (suit.equals(Suit.DIAMONDS)) {
            System.out.println("Suit is DIAMONDS");
        } else {
            System.out.println("Suit is NOT DIAMONDS");
        }
 
        // compareTo
        if (suit.compareTo(Suit.HEARTS) > 0) {
            System.out.println("Suit is bigger than HEATS");
        } else if (suit.compareTo(Suit.HEARTS) < 0) {
            System.out.println("Suit is less than HEATS");
        } else {
            System.out.println("Suit is HEATS");
        }

コンパイルは問題なくできます。equals、compareTo はクラスしか使えないので、enum は明らかにクラスとして扱われていることが分かります。

そして、この部分の実行結果は次のようになりました。

Suit is DIAMONDS
Suit is less than HEATS

equals メソッドは使えそうだということは分かりますが、compareTo が使えるのは結構以外でした。どうやら順序は定義の並び順になっているようです。これがどうしてこうなるかはもう少し後に調べてみましょう。

 

switch

次は switch 文で使えるかどうかです。普通、switch 文は int や char など整数系のプリミティブ型しか使えません。enum ではどうでしょう。

サンプルのソース EnumTest2.java

 

public class EnumTest2 {
    public enum Num {ONE, TWO, THREE, FOUR}
 
    public static void main(String[] args) {
        Num num = Num.ONE;
 
        switch (num) {
          case ONE:
            System.out.println("One");
            break;
          case TWO:
            System.out.println("Two");
            break;
          case THREE:
            System.out.println("Three");
            break;
          case FOUR:
            System.out.println("Four");
            break;
        }
    }
}

case 文には直接 enum の定数名を記述します (註)

コンパイルしてみるとちゃんとできます。実行もできました。

C:\JSR201\examples>javac EnumTest2.java
 
C:\JSR201\examples>javac EnumTest2.java
One
 
C:\JSR201\examples>

switch 文で使えるのはかなりうれしいです。Effective Java に出ていた Typesafe Enum だと switch は使えないので、延々と if ... else if ... else if ... とつなげなくてはいけないので、見にくくてしょうがなかったのです。

 

(註) Generics の Early Access 版の時には switch 文の case の書き方は case Num.ONE: のように enum の型名まで書かなくてはいけませんでしたが、簡略化されたようです。

 

表示

前述したように、enum の変数は単に出力しただけでもちゃんと文字列として出力されていました。もう一度、繰り返してみましょう。

サンプルのソース EnumTest3.java

 

public class EnumTest3 {
    public enum Num {ONE, TWO, THREE, FOUR}
 
    public static void main(String[] args) {
        Num num = Num.ONE;
 
        System.out.println("Num is " + num);
        System.out.println("TWO is " + Num.TWO);
    }
}

実行すると enum の定数と同じ文字列が表示されます。

C:\JSR201\examples>java EnumTest3
Num is ONE
TWO is TOW

C:\JSR201\examples>

static final な定数を出力しても int だったら数値しか出力されないので、その値と定数を対比させるのが面倒だったのですが、これならば簡単です。

 

 
 
Tiger どうやっているのだろう
 
 

Effecrive Java でどのように Typesafe Enum が構築されているか見れば、大体の予想はつきます。何らかのクラスを使って、定数はそのクラスのオブジェクトになっているに違いません。

実際はどうなっているのでしょう。さっそく調べてみます。逆コンパイラの Jad を使って前述の EnumTest3 クラスを逆コンパイルしてみました。

public class EnumTest3
{
    public static class Num extends Enum
    {
 
        public static final Num[] values()
        {
            return (Num[])$VALUES.clone();
        }
 
        public static Num valueOf(String s)
        {
            Num anum[] = $VALUES;
            int i = anum.length;
            for(int j = 0; j < i; j++)
            {
                Num num = anum[j];
                if(num.name().equals(s))
                    return num;
            }
 
            throw new IllegalArgumentException(s);
        }
 
        public int compareTo(Enum enum)
        {
            return super.compareTo((Num)enum);
        }
 
        public int compareTo(Object obj)
        {
            return super.compareTo((Num)obj);
        }
 
        public static final Num ONE;
        public static final Num TWO;
        public static final Num THREE;
        public static final Num FOUR;
        private static final Num $VALUES[];
 
        static 
        {
            ONE = new Num("ONE", 0);
            TWO = new Num("TWO", 1);
            THREE = new Num("THREE", 2);
            FOUR = new Num("FOUR", 3);
            $VALUES = (new Num[] {
                ONE, TWO, THREE, FOUR
            });
        }
 
        public Num(String s, int i)
        {
            super(s, i);
        }
    }
 
    public EnumTest3()
    {
    }
 
    public static void main(String args[])
    {
        Num num = Num.ONE;
        System.out.println("Num is " + num);
        System.out.println("TWO is " + Num.TWO);
    }
}

もともと 10 行のプログラムがなんと 6 倍以上の行数になってしまいました ^^;;

もう少し詳しく見てみましょう。EnumTest3 の enum の定義している行はどうなっているのでしょう。もともとはこうですが、

    public enum Num {ONE, TWO, THREE, FOUR}

逆コンパイルしたソースではこの部分は次のようになっています。

        public static final Num ONE;
        public static final Num TWO;
        public static final Num THREE;
        public static final Num FOUR;
        private static final Num $VALUES[];
 
        static 
        {
            ONE = new Num("ONE", 0);
            TWO = new Num("TWO", 1);
            THREE = new Num("THREE", 2);
            FOUR = new Num("FOUR", 3);
            $VALUES = (new Num[] {
                ONE, TWO, THREE, FOUR
            });
        }

enum の Num が Num クラスとして扱われているようです。 そして、ONE, TWO などは Num クラスのオブジェクトになっています。面白いのはコンストラクタの引数に変数の名前がそのまま文字列として使われていることです。

ONE, TWO などを表示したときに出力される文字列はこのあたりに工夫があるようですね。

もう 1 つ $VALUES という定数が定義されています。この定数は定義された enum の定数を要素に持つ配列として定義されているようです。具体的に $VALUES が何かはもう少し後で見てみましょう。

さて、ONE, TWO などが Num クラスのオブジェクトだということは分かりましたが、Num クラスとは何なんでしょう。EnumTest3 の内部クラスとして定義されているようです。

    public static class Num extends Enum

定義を見ると、Enum クラスの派生クラスとなっています。この Enum クラスは src.zip の中にソースがあります。コメントがあると長いので、コメントを抜いて次に示します。

package java.lang;
 
import java.io.Serializable;
 
public class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    private final String name;
 
    public final String name() {
        return name;
    }
 
    private final int ordinal;
 
    public final int ordinal() {
        return ordinal;
    }
 
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
 
    public String toString() {
        return name;
    }
 
    final public boolean equals(Object other) { 
        return this==other;
    }
 
    final public int hashCode() {
        return System.identityHashCode(this);
    }
 
    final protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
 
    public int compareTo(E o) {
        Enum other = (Enum)o;
        Enum self = this;
        if (self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }
 
    public final Class<E> getDeclaringClass() {
        Class clazz = getClass();
        Class zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? clazz : zuper;
    }
}

Enum クラスはプロパティとして文字列の name と int の ordinal を持っていることが分かります。先ほど、Num クラスのコンストラクタで変数名と同じ文字列を引数にしていましたが、それが name に代入されています。

toString メソッドはこの name を戻しているので、System.out などに出力すると変数名が出力できたわけです。

もう 1 つのプロパティ ordinal もコンストラクタで指定されています。もともとの Num で ONE や TWO を生成するときに、変数の並びと同じように ordinal が 0 から指定されています。

この ordinal が compareTo メソッドで比較するときに使われているようです。Enum クラスの compareTo メソッドを見ると、ordinal の引き算を行っているのが分かると思います。

さて、もう一度 Num クラスに戻って、定数の $VALUES を見てみましょう。$VALUES が使われているのは values メソッドと valueOf メソッドです。

        public static final Num[] values()
        {
            return (Num[])$VALUES.clone();
        }
 
        public static Num valueOf(String s)
        {
            Num anum[] = $VALUES;
            int i = anum.length;
            for(int j = 0; j < i; j++)
            {
                Num num = anum[j];
                if(num.name().equals(s))
                    return num;
            }
 
            throw new IllegalArgumentException(s);
        }

values メソッドは $VALUES のコピーを返すようになっています。これを使えば、enum のループなどが実現できそうです。

また、valueOf メソッドは文字列を引数にすると、それと同じ名前の enum 定数を返してくれます。これで 文字列 -> enum 定数 の変換が行うことができるわけです。

(註) Generics の Early Access 版では $VALUES は VALUES という名前の public な定数になっていました。しかし、オブジェクト指向っぽくないからか values メソッドを使用してアクセスさせるように変更されたようです。また、name や ordinary も public でしたが、両方とも private に変更され、アクセスメソッドが追加されました。

values メソッドを使ってループを作ってみましょう。

サンプルのソース EnumTest4.java

values メソッドの戻り値は配列なので、拡張 for 文を使うことができます。

public class EnumTest4 {
    public enum Num {ONE, TWO, THREE, FOUR}
 
    public static void main(String[] args) {
        System.out.println("Num.VALUES's Class : " + Num.values().getClass());
        for (Num num : Num.values()) {
            System.out.println("Num is " + num);
        }
    }
}

さっそく実行です。

C:\JSR201\examples>java EnumTest4
Num.$VALUES's Class : class [LEnumTest4$Num;
Num is ONE
Num is TWO
Num is THREE
Num is FOUR

C:\JSR201\examples>

クラスのところで示されているのは JNI などで使用されるシグネチャーで [ が配列を表しています。その後の L の後の ; の前までがクラス名です。$ は内部クラスであることを示しているので、EnumTest4 クラスの内部クラス Num の配列ということになります。

ついでなので、ordinal がどうなっているのか見てみましょう。

サンプルのソース EnumTest5.java

 

public class EnumTest5 {
    public enum Num {ONE, TWO, THREE, FOUR}
 
    public static void main(String[] args) {
        for (Num num : Num.values()) {
            System.out.println("Num[" + num + "]'s ordinal is " + num.ordinal());
        }
    }
}

実行すると、ちゃんと ordinal が 0 から割り振られていることが分かります。

C:\JSR201\examples>java EnumTest5
Num[ONE]'s ordinal is 0
Num[TWO]'s ordinal is 1
Num[THREE]'s ordinal is 2
Num[FOUR]'s ordinal is 3

C:\JSR201\examples>

ordinary を直接使うことはないと思いますが、compreTo が使えるのは結構うれしいかもしれません。

ついでといってはなんですが、switch 文はどのように解釈されているのでしょうか。EnumTest2 クラスを逆コンパイルしてみました。enum の定義の部分は EnumTest3 クラスと一緒なので、main メソッドだけを次に示します。

    public static void main(String args[])
    {
        Num num = Num.ONE;
        Num num1 = num;
        if(num1 != Num.ONE)
        {
            if(num1 != Num.TWO)
            {
                if(num1 != Num.THREE)
                {
                    if(num1 == Num.FOUR)
                        System.out.println("Four");
                } else
                {
                    System.out.println("Three");
                }
            } else
            {
                System.out.println("Two");
            }
        } else
        {
            System.out.println("One");
        }

これは結構ビックリじゃないですか。switch 文が展開されて if 文になっているなんて... これはちょっと予想外でした。ordinary を使って switch 文の case のところを記述するのではないかと思っていたからです。

まぁ、確かにこれでも動くんですけど、ちょっとなぁ...

 

 
 
Tiger 便利な使い方あれこれ メソッドを定義する
 
 

enum はなかなか奥が深くて、まだまだ面白いことができそうです。

なんと定数なのにメソッドが定義できてしまうのです。Enum クラスでメソッドが定義できるのは当たり前ですが、enum のままでもメソッドの定義ができるのです。

なにはともあれ、早速やってみましょう。まずは toString メソッドをオーバライドしてみます。

サンプルのソース EnumTest6.java

 

public class EnumTest6 {
    public enum Num {ONE, TWO, THREE, FOUR;
        public String toString() {
            return name().substring(0,1).toUpperCase() 
                + name().substring(1).toLowerCase();
        }
    }
 
    public static void main(String[] args) {
        for (Num num : Num.values()) {
            System.out.println("Num is " + num);
        }
    }
}

enum でのメソッドの書き方は定数の並びの後に ";" を記述し、その後にメソッドを書くようにします。

toString メソッドの中はたいしたことはやっていません。1文字目だけを大文字に、後は小文字に変換しています。

これを実行すると次のようになり、デフォルトの toString メソッドとはふるまいが変わることがわかります。

C:\JSR201\examples>java EnumTest6
Num is One
Num is Two
Num is Three
Num is Four

C:\JSR201\examples>

次はオーバライドではなくて、普通のメソッドを書いてみましょう。

サンプルのソース EnumTest7.java

increment メソッドを定義してみました。このメソッドをコールすると、自分より 1 つ大きい enum の定数を返します。

public class EnumTest7 {
    public enum Num {ONE, TWO, THREE, FOUR;
        public Num increment() {
            int next = ordinal() + 1;
            return values()[next];
        }
    }
 
    public static void main(String[] args) {
        for (Num num = Num.ONE; num.compareTo(Num.FOUR) < 0; num = num.increment()) {
            System.out.println("Num is " + num);
        }
    }
}

普通のメソッドでもオーバライドでも書き方は同じです。あまり enum ということは意識せずに、普通にメソッドを欠くことができます。

さて、実行です。

C:\JSR201\examples>java EnumTest7
Num is ONE
Num is TWO
Num is THREE

C:\JSR201\examples>

THREE までなのは、手抜きのためです ^^;;

今までは状態を持たないメソッドでしたが、状態を持つ、いいかえればプロパティを持つようなメソッドも書くことができます。それだけでなく、なんとコンストラクタも定義することができます。

たとえばこんな例です。

サンプルのソース EnumTest8.java

コンストラクタへの引数は enum の定数の後にカッコを書いてそこに記述します。ここでは int の引数が 1 つです。

public class EnumTest8 {
    private enum Num {ONE(1), TWO(2), THREE(3), FOUR(4);
        private final int value;
 
        Num(int value) {
            this.value = value;
        }
 
        public int getValue() {
            return value;
        }
    }
 
    public static void main(String[] args) {
        for (Num num: Num.values()) {
            System.out.println(num + " is " + num.getValue());
        }
    }
}

プロパティとして value という値を持ち、getValue メソッドはその値を返すようにしました。ordinary だとどんな値になるか分からないのですが、これだと対応する値がすぐにわかります。

実行すると、次のようになります。

C:\JSR201\examples>java EnumTest8
ONE is 1
TWO is 2
THREE is 3
FOUR is 4

C:\JSR201\examples>

EnumTest8 のようなことをやると、もしかしてデザインパターンの State パターンができるのではという淡い期待が湧いてきました。でも、State パターンは状態を表わす派生クラスでふるまいの違うメソッドを定義することになっています。

EnumTest8 クラスの場合で考えれば ONE や TWO が Num クラスの派生クラスとなればいいのですが、このままじゃ無理そうです。

そこでそいうのが許されていないか、仕様書をよく見てみたら、ありました。ONE も TWO も派生クラスになりそうです。そこで、さっそく次のような例を考えてみました。

サンプルのソース EnumTest9.java

State パターンなので状態遷移を行うのですが、この例では START -> RUNNING -> STOP という単純な状態遷移を扱っています。

State の定義のところで abstract な次の状態を返す nextState メソッドを定義しておき、そのインプリを各定数の定義の中に書いています。

public class EnumTest9 {
    private enum State {
        START  {
            public State nextState() {
                return State.RUNNING;
            }
        },
 
        RUNNING {
            public State nextState() {
                return State.STOP;
            }
        },
 
        STOP {
            public State nextState() {
                return State.STOP;
            }
        };
 
        public abstract State nextState();
    }
 
    public static void main(String[] args) {
        State state = State.START;
        
        System.out.println("Present State is " + state);
        state = state.nextState();
        System.out.println("Next State is " + state);
        state = state.nextState();
        System.out.println("Last State is " + state);
    }
}

alpha ではこれはコンパイルすら通らなかったのですが、beta ではさくっとコンパイルできました。実行すると、ちゃんと状態遷移をしています。

C:\JSR201\examples>java EnumTest9
Present State is START
Next State is RUNNING
Last State is STOP

C:\JSR201\examples>

Generics の Early Access 版ではできなかったインタフェースのインプリメントもできるようになりました。

こんな使い方はまずしないと思いますが、Iterator オブジェクトを返せるようにしてみました。

サンプルのソース EnumTest10.java

 

import java.util.Iterator;

public class EnumTest10 {
    private enum Num implements Iterable {ONE, TWO, THREE, FOUR;
        public Iterator iterator() {
            return new Iterator<Num>() {
                private int position = 0;
 
                public boolean hasNext() {
                    return position < Num.values().length;
                }
 
                public Num next() {
                    Num num =  Num.values()[position];
                    position++;
                    
                    return num;
                }
 
                public void remove() {
                    throw new UnsupportedOperationException();
                }
            };
        }
    }
 
    public static void main(String[] args) {
        Num num = Num.ONE;
        Iterator it = num.iterator();
 
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

インタフェースをインプリメントするには enum の型名の後に implements インタフェース名 と記述します。EnumTest10 クラスでは Iterable インタフェースをインプリメントして、iterator メソッドを定義しています。

 

 
 
Tiger 便利なクラスもあるよ
 
 

enum と一緒に導入されたクラスに java.util.EnumSet クラスと java.util.EnumMap クラスがあります。

これらのクラスは enum 型を使うという前提に立っているので、なかなか面白いです。

ところで、Effective Java にはタイプセーフ enum と static final int を比較した場合、欠点として 2 つのことが書かれています。

  1. switch 文で使えない
  2. ビット演算ができない

Tiger の enum 型が switch 文でも使えるのは前述したとおりです。

そして、もうひとつのビット演算ができないことをカバーするのが EnumSet クラスなのです。

ビット演算はフラグ処理などでよく使われる手法です。たとえば、こんな風に使われます。

    int ONE_BIT = 0x01;     // 0000 0001
    int TWO_BIT = 0x02;     // 0000 0010
    int THREE_BIT = 0x04;   // 0000 0100
    int FOUR_BIT = 0x08;    // 0000 1000
 
    int pattern = ONE_BIT | THREE_BIT | FOUR_BIT;  // 0000 1101
 
    int x = ONE_BIT;
    boolean result = ((x & pattern) != 0);

    x = TWO_BIT;
    result = ((x & pattern) != 0);

このようにフラグビットを設定しておいて入力 (この場合は x) と比較するなんてことがよく行われます。

実際の例だと java.awt.Component.enableEvents メソッドなどがこれに相当します。このメソッドの引数は long ですが、実際には AWTEvent で定義されているマスクの "OR" をとったものを使用します。

たとえば、マウスイベントとキーイベントを登録したかったら次のように指定します。

    Component component = new TextField(10); 
 
    component.enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);

enum はオブジェクトなので、こういう使い方はできません。そこで、導入されたのが EnumSet クラスです。

EnumSet クラスは要素に enum しか入れることができません。また new で生成するのではなくファクトリメソッドを使用します。

サンプルのソース EnumSetTest.java

EnumSet クラスにはファクトリメソッドが多く用意されています。

メソッド名 説明
of 指定したフラグビットを立てた HashSet オブジェクトの生成
引数が 1 から 5 個までのメソッドが用意されている
range 指定された 2 つの定数の間のフラグを立てた HashSet オブジェクトの生成
allOf enum のすべての定数に対応するフラグビットが立っている HashSet オブジェクトの生成
noneOf フラグビットが 1 つもたっていない HashSet オブジェクトの生成

これ以外にコピーコンストラクタもありますが、基本的には上の 3 つのメソッドが使われると思います。

EnumSetTest クラスでは BitFlag という enum を定義しています。これがフラグビットの位置を決めます。

of は引数の数により 5 種類用意されていますが、ここでは 3 種類使用しています。ビットを立てるものを引数とするので、引数が 3 つであれば 3 つのフラグが立った状態になります。

range メソッドは範囲指定をするメソッドです。必ず、定義順で前のものを第 1 引数にします。

allOf メソッドと noneOf メソッドは、すべてを立てるか、1 つも立てない場合に使用されます。

public class EnumSetTest {
    enum BitFlags {ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT}
  
    private EnumSet bitSet;
  
    public EnumSetTest() {
        // 0000 0001 を表すビットパターン
        bitSet = EnumSet.of(BitFlag.ONE);
        checkIncluded(bitSet);
  
        // 0000 0011 を表すビットパターン
        bitSet = EnumSet.of(BitFlag.TWO, BitFlag.ONE);
        checkIncluded(bitSet);
   
        // 1001 1100 を表すビットパターン
        bitSet = EnumSet.of(BitFlag.EIGHT, 
                            BitFlag.FIVE,
                            BitFlag.FOUR,
                            BitFlag.THREE);
        checkIncluded(bitSet);
  
        // 0111 1000 を表すビットパターン
        bitSet = EnumSet.range(BitFlag.FOUR, BitFlag.SEVEN);
        checkIncluded(bitSet);
 
        // 1111 1111 を表すビットパターン
        bitSet = EnumSet.allOf(BitFlag.class);
        checkIncluded(bitSet);
  
        // 0000 0000 を表すビットパターン
        bitSet = EnumSet.noneOf(BitFlag.class);
        checkIncluded(bitSet);
    }

生成した HashSet オブジェクトと変数の値のチェックには contains メソッドを使用します。

    public void checkIncluded(EnumSet bitSet) {
        System.out.println("BitSet: " + bitSet);
        for (BitFlag pattern : BitFlag.values()) {
            System.out.println(pattern + " is included in BitSet: " 
                               + bitSet.contains(pattern));
        }
        System.out.println();
    }

これを実行してみると次のようになりました。

C:\JSR201\examples>java EnumSetTest
BitSet: [ONE]
ONE is included in BitSet: true
TWO is included in BitSet: false
THREE is included in BitSet: false
FOUR is included in BitSet: false
FIVE is included in BitSet: false
SIX is included in BitSet: false
SEVEN is included in BitSet: false
EIGHT is included in BitSet: false

class java.util.RegularEnumSet
BitSet: [ONE, TWO]
ONE is included in BitSet: true
TWO is included in BitSet: true
THREE is included in BitSet: false
FOUR is included in BitSet: false
FIVE is included in BitSet: false
SIX is included in BitSet: false
SEVEN is included in BitSet: false
EIGHT is included in BitSet: false

BitSet: [THREE, FOUR, FIVE, EIGHT]
ONE is included in BitSet: false
TWO is included in BitSet: false
THREE is included in BitSet: true
FOUR is included in BitSet: true
FIVE is included in BitSet: true
SIX is included in BitSet: false
SEVEN is included in BitSet: false
EIGHT is included in BitSet: true

BitSet: [FOUR, FIVE, SIX, SEVEN]
ONE is included in BitSet: false
TWO is included in BitSet: false
THREE is included in BitSet: false
FOUR is included in BitSet: true
FIVE is included in BitSet: true
SIX is included in BitSet: true
SEVEN is included in BitSet: true
EIGHT is included in BitSet: false

BitSet: [ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT]
ONE is included in BitSet: true
TWO is included in BitSet: true
THREE is included in BitSet: true
FOUR is included in BitSet: true
FIVE is included in BitSet: true
SIX is included in BitSet: true
SEVEN is included in BitSet: true
EIGHT is included in BitSet: true

BitSet: []
ONE is included in BitSet: false
TWO is included in BitSet: false
THREE is included in BitSet: false
FOUR is included in BitSet: false
FIVE is included in BitSet: false
SIX is included in BitSet: false
SEVEN is included in BitSet: false
EIGHT is included in BitSet: false
 
 
C:\JSR201\examples>

これだけだと特に面白いところはありません。面白いのは HashSet クラスのソースなのです。

たとえば、of メソッドを見てみましょう。

    public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) {
        EnumSet result = noneOf(e1.getDeclaringClass());
        result.add(e1);
        result.add(e2);
        return result;
    }

noneOf メソッドを使用してオブジェクトを生成してから、要素を add しています。そこで、add メソッドを見てみると EnumSet クラスには定義されていません。EnumSet クラスは対象とする enum 型の定数の数によって生成するクラスを変化させています。64 以下であれば RegularEnumSet クラス、それ以上は JumboEnumSet クラスになります。

RegularEnumSet クラスの add メソッドは

    public boolean add(E e) {
        long oldElements = elements;
        elements |= (1L << ((Enum)e).ordinal());
        return elements != oldElements;
    }

なんと long の変数 elemens でビットフラグをあらわすようになっています。ビットの位置は enum の ordinal を利用しています。

普通に enum を使用するとパフォーマンス的に不利であるため、static final int で定義された定数を使用した場合と同じことをしているわけです。

とすると contains メソッドもこれに応じて変更されているかもしれません。

    public boolean contains(Object e) {
        if (e == null)
            return false;
        Class eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;

        return (elements & (1L << ((Enum)e).ordinal())) != 0;
    }

注目すべきは最後の return 文の行です。やはり long を使ったビットの AND 処理を使用しています。これで高速にフラグ処理を行えるわけですね。

先ほど enum の定数の数によりクラスを変えていることをかきましたが、その理由はビットフラグに long を使用しているからのようです。64 以下であれば 1 つの long で表すことができますが、それ以上は複数の long が必要になるからです。

そして、JumboEnumSet クラスは要素を long の配列で持つようになっています。

 

次に EnumMap クラスです。EnumMap クラスはキーが enum に限定されているマップです。

使い方はキーが enum だという以外は通常のマップと同じです。このクラスも enum に特化した実装が行われています。たとえば、HashMap クラスでは要素を格納するのに Entry クラスの配列を使用しますが、EnumMap クラスでは単に Object クラスの配列が使用されます。

というのは、enum を使用した場合、ハッシュ値を演算するより ordinal を使ったほうが効率がいいからです。たとえば、HashMap クラスの get メソッドは次のように実装されています。

  1. キーのハッシュ値を求める (Object#hashCode メソッドを使用)
  2. ハッシュ値をインデックスとして要素の配列から Entry オブジェクトを取り出す
  3. Entry オブジェクトの value を戻す

これが EnumMap クラスの場合は

  1. キーの ordinal を取得する
  2. ordinal をインデックスとして要素の配列からオブジェクトを取り出す
  3. 得られたオブジェクトを戻す

ということを行っています。

なぜこんなことができるのでしょうか。

ハッシュ値の変わりに ordinal を使用できることもそうですが、キーが enum ということは enum で定義された定数以上にマップに保持させる要素が増えないことが保証されているからです。

この 2 つのクラスは、クラスのヘッダによると Joshua Bloch が自ら書いているようです。達人のエッセンスを盗むべく、ソースを見てみるのも面白いと思いますよ。

 
 
Tiger おわりに
 
 

enum 型はなかなか便利です。final static な定数よりもこちらを使うほうがいろいろな面でメリットがあります。

ただプリミティブ型の定数と比べるとパフォーマンス的には不利になりますが、微々たるものだと思います。

ぜひ、その便利さを使ってみて実感してみてください。

 

今回使用したサンプルはここからダウンロードできます。

参考

(Dec. 2003)
(改訂 J2SE 1.5beta に関する変更 Jan. 2004)
(改訂 EnumSet クラス, EnumMap クラスを追加 Apr. 2004)
(改訂 Aug. 2005)

 
 
Go to Contents Go to Java Page