|
Typesafe Enum |
|||||
|
|||||
Java で C/C++ の enum 型がないと嘆いている方は多いのではないでしょうか。 単に enum を導入するのは私は反対でした。というのも、enum 型には潜在的な問題があると思うからです。enum 型は結局 int 型と同じに扱われてしまうので、範囲のチェックなどやってくれません。たとえば
また、enum をごっちゃにしても言語仕様的には大丈夫なのです。
このままだと安全が言語仕様に取り込まれている Java には似合わないと思うのです。 でも、結局 enum の代わりに static なクラス変数を使用していたら同じ問題が起きてしまいます。
このような問題がない enum なんてあるのでしょうか。あるのです。 それがタイプセーフ Enum です。
|
|
||||||
タイプセーフ Enum は実をいうと Joshua Bloch さんが Effective Java という本の中で提案されているものなんです。ちなみに Joshua Bloch さんは JSR-201 の Spec Lead でもあります。 Effective Java の中ではタイプセーフ Enum はどのように説明されているのか簡単に紹介します。 タイプセーフ Enum の中心的なアイディアは定数を表わすためのクラスを使って、各定数はそのオブジェクトにしてしまうというのものです。ただ、勝手に new されてしまっても困るので、コンストラクタは private にしてしまいます。 Effective Java の中で示されている例は次のようなものです。
これで完璧かと思うとまだまだなのです。このタイプセーフ Enum をシリアライズするにはどうすればいいかとか、振る舞いを持たせるにはどうすればいいかなどの議論が続きます。 完璧なタイプセーフ Enum を作るにはかなり面倒なのです。 しかし、Tiger でタイプセーフ Enum が導入されたことでそんなことを気にせずに enum を使用することができるようになったのです。 上の Suit クラスを enum で表わすと次のようになります。
enum というのが新しい予約語です。この定義をすれば Suit を型のように使うことができます。そして、enum の要素は頭に Suit をつけて、Suit.CLUBS などのように表わすことができます。 さっそく使ってみましょう。
これを実行すると
なにも設定していないのに、出力すると CLUBS となります。普通の整数の定数だと、その値が出力されてしまい、それが実際にどれに対応するか調べる必要があったのですが、これだととても楽になります。
|
|
||||||||||||||||||||
正しい enum の書き方をいちおう示しておきましょう。まずは基本的な書き方。
ClassModifier は public や static、final などです。これは省略可能です。 Enum にしたい定数は "," でつなげて記述します。たとえばこんな感じです。
さっそく書いてみましょう。
まずは定義です。
代入 定義ができたところで、代入してみましょう。Enum は一種の型のように扱うことができるので、次のように書くことができます。
当たり前ですが、できます ^^;; これも当たり前ですが、C/C++ の enum と違って int に代入することはできません。コンパイルエラーになります。
比較 代入ができたので、つづいて比較してみましょう。まずは == が使えるかどうかを試してみます。
この部分の実行結果は
となります。== は使えることは予想通りですが、equals は使えるか試してみましょう。ついでに、compareTo も試してみます。
コンパイルは問題なくできます。equals、compareTo はクラスしか使えないので、enum は明らかにクラスとして扱われていることが分かります。 そして、この部分の実行結果は次のようになりました。
equals メソッドは使えそうだということは分かりますが、compareTo が使えるのは結構以外でした。どうやら順序は定義の並び順になっているようです。これがどうしてこうなるかはもう少し後に調べてみましょう。
switch 次は switch 文で使えるかどうかです。普通、switch 文は int や char など整数系のプリミティブ型しか使えません。enum ではどうでしょう。
case 文には直接 enum の定数名を記述します (註)。 コンパイルしてみるとちゃんとできます。実行もできました。
switch 文で使えるのはかなりうれしいです。Effective Java に出ていた Typesafe Enum だと switch は使えないので、延々と if ... else if ... else if ... とつなげなくてはいけないので、見にくくてしょうがなかったのです。
(註) Generics の Early Access 版の時には switch 文の case の書き方は case Num.ONE: のように enum の型名まで書かなくてはいけませんでしたが、簡略化されたようです。
表示 前述したように、enum の変数は単に出力しただけでもちゃんと文字列として出力されていました。もう一度、繰り返してみましょう。
実行すると enum の定数と同じ文字列が表示されます。
static final な定数を出力しても int だったら数値しか出力されないので、その値と定数を対比させるのが面倒だったのですが、これならば簡単です。
|
|
|||||||||||||||||
Effecrive Java でどのように Typesafe Enum が構築されているか見れば、大体の予想はつきます。何らかのクラスを使って、定数はそのクラスのオブジェクトになっているに違いません。 実際はどうなっているのでしょう。さっそく調べてみます。逆コンパイラの Jad を使って前述の EnumTest3 クラスを逆コンパイルしてみました。
もともと 10 行のプログラムがなんと 6 倍以上の行数になってしまいました ^^;; もう少し詳しく見てみましょう。EnumTest3 の enum の定義している行はどうなっているのでしょう。もともとはこうですが、
逆コンパイルしたソースではこの部分は次のようになっています。
enum の Num が Num クラスとして扱われているようです。 そして、ONE, TWO などは Num クラスのオブジェクトになっています。面白いのはコンストラクタの引数に変数の名前がそのまま文字列として使われていることです。 ONE, TWO などを表示したときに出力される文字列はこのあたりに工夫があるようですね。 もう 1 つ $VALUES という定数が定義されています。この定数は定義された enum の定数を要素に持つ配列として定義されているようです。具体的に $VALUES が何かはもう少し後で見てみましょう。 さて、ONE, TWO などが Num クラスのオブジェクトだということは分かりましたが、Num クラスとは何なんでしょう。EnumTest3 の内部クラスとして定義されているようです。
定義を見ると、Enum クラスの派生クラスとなっています。この Enum クラスは src.zip の中にソースがあります。コメントがあると長いので、コメントを抜いて次に示します。
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 メソッドです。
values メソッドは $VALUES のコピーを返すようになっています。これを使えば、enum のループなどが実現できそうです。 また、valueOf メソッドは文字列を引数にすると、それと同じ名前の enum 定数を返してくれます。これで 文字列 -> enum 定数 の変換が行うことができるわけです。 (註) Generics の Early Access 版では $VALUES は VALUES という名前の public な定数になっていました。しかし、オブジェクト指向っぽくないからか values メソッドを使用してアクセスさせるように変更されたようです。また、name や ordinary も public でしたが、両方とも private に変更され、アクセスメソッドが追加されました。 values メソッドを使ってループを作ってみましょう。
values メソッドの戻り値は配列なので、拡張 for 文を使うことができます。
さっそく実行です。
クラスのところで示されているのは JNI などで使用されるシグネチャーで [ が配列を表しています。その後の L の後の ; の前までがクラス名です。$ は内部クラスであることを示しているので、EnumTest4 クラスの内部クラス Num の配列ということになります。 ついでなので、ordinal がどうなっているのか見てみましょう。
実行すると、ちゃんと ordinal が 0 から割り振られていることが分かります。
ordinary を直接使うことはないと思いますが、compreTo が使えるのは結構うれしいかもしれません。 ついでといってはなんですが、switch 文はどのように解釈されているのでしょうか。EnumTest2 クラスを逆コンパイルしてみました。enum の定義の部分は EnumTest3 クラスと一緒なので、main メソッドだけを次に示します。
これは結構ビックリじゃないですか。switch 文が展開されて if 文になっているなんて... これはちょっと予想外でした。ordinary を使って switch 文の case のところを記述するのではないかと思っていたからです。 まぁ、確かにこれでも動くんですけど、ちょっとなぁ...
|
|
|||||||||||||||||||||
enum はなかなか奥が深くて、まだまだ面白いことができそうです。 なんと定数なのにメソッドが定義できてしまうのです。Enum クラスでメソッドが定義できるのは当たり前ですが、enum のままでもメソッドの定義ができるのです。 なにはともあれ、早速やってみましょう。まずは toString メソッドをオーバライドしてみます。
enum でのメソッドの書き方は定数の並びの後に ";" を記述し、その後にメソッドを書くようにします。 toString メソッドの中はたいしたことはやっていません。1文字目だけを大文字に、後は小文字に変換しています。 これを実行すると次のようになり、デフォルトの toString メソッドとはふるまいが変わることがわかります。
次はオーバライドではなくて、普通のメソッドを書いてみましょう。
increment メソッドを定義してみました。このメソッドをコールすると、自分より 1 つ大きい enum の定数を返します。
普通のメソッドでもオーバライドでも書き方は同じです。あまり enum ということは意識せずに、普通にメソッドを欠くことができます。 さて、実行です。
THREE までなのは、手抜きのためです ^^;; 今までは状態を持たないメソッドでしたが、状態を持つ、いいかえればプロパティを持つようなメソッドも書くことができます。それだけでなく、なんとコンストラクタも定義することができます。 たとえばこんな例です。
コンストラクタへの引数は enum の定数の後にカッコを書いてそこに記述します。ここでは int の引数が 1 つです。
プロパティとして value という値を持ち、getValue メソッドはその値を返すようにしました。ordinary だとどんな値になるか分からないのですが、これだと対応する値がすぐにわかります。 実行すると、次のようになります。
EnumTest8 のようなことをやると、もしかしてデザインパターンの State パターンができるのではという淡い期待が湧いてきました。でも、State パターンは状態を表わす派生クラスでふるまいの違うメソッドを定義することになっています。 EnumTest8 クラスの場合で考えれば ONE や TWO が Num クラスの派生クラスとなればいいのですが、このままじゃ無理そうです。 そこでそいうのが許されていないか、仕様書をよく見てみたら、ありました。ONE も TWO も派生クラスになりそうです。そこで、さっそく次のような例を考えてみました。
State パターンなので状態遷移を行うのですが、この例では START -> RUNNING -> STOP という単純な状態遷移を扱っています。 State の定義のところで abstract な次の状態を返す nextState メソッドを定義しておき、そのインプリを各定数の定義の中に書いています。
alpha ではこれはコンパイルすら通らなかったのですが、beta ではさくっとコンパイルできました。実行すると、ちゃんと状態遷移をしています。
Generics の Early Access 版ではできなかったインタフェースのインプリメントもできるようになりました。 こんな使い方はまずしないと思いますが、Iterator オブジェクトを返せるようにしてみました。
インタフェースをインプリメントするには enum の型名の後に implements インタフェース名 と記述します。EnumTest10 クラスでは Iterable インタフェースをインプリメントして、iterator メソッドを定義しています。
|
|
||||||||||||||||||||||
enum と一緒に導入されたクラスに java.util.EnumSet クラスと java.util.EnumMap クラスがあります。 これらのクラスは enum 型を使うという前提に立っているので、なかなか面白いです。 ところで、Effective Java にはタイプセーフ enum と static final int を比較した場合、欠点として 2 つのことが書かれています。
Tiger の enum 型が switch 文でも使えるのは前述したとおりです。 そして、もうひとつのビット演算ができないことをカバーするのが EnumSet クラスなのです。 ビット演算はフラグ処理などでよく使われる手法です。たとえば、こんな風に使われます。
このようにフラグビットを設定しておいて入力 (この場合は x) と比較するなんてことがよく行われます。 実際の例だと java.awt.Component.enableEvents メソッドなどがこれに相当します。このメソッドの引数は long ですが、実際には AWTEvent で定義されているマスクの "OR" をとったものを使用します。 たとえば、マウスイベントとキーイベントを登録したかったら次のように指定します。
enum はオブジェクトなので、こういう使い方はできません。そこで、導入されたのが EnumSet クラスです。 EnumSet クラスは要素に enum しか入れることができません。また new で生成するのではなくファクトリメソッドを使用します。
EnumSet クラスにはファクトリメソッドが多く用意されています。
これ以外にコピーコンストラクタもありますが、基本的には上の 3 つのメソッドが使われると思います。 EnumSetTest クラスでは BitFlag という enum を定義しています。これがフラグビットの位置を決めます。 of は引数の数により 5 種類用意されていますが、ここでは 3 種類使用しています。ビットを立てるものを引数とするので、引数が 3 つであれば 3 つのフラグが立った状態になります。 range メソッドは範囲指定をするメソッドです。必ず、定義順で前のものを第 1 引数にします。 allOf メソッドと noneOf メソッドは、すべてを立てるか、1 つも立てない場合に使用されます。
生成した HashSet オブジェクトと変数の値のチェックには contains メソッドを使用します。
これを実行してみると次のようになりました。
これだけだと特に面白いところはありません。面白いのは HashSet クラスのソースなのです。 たとえば、of メソッドを見てみましょう。
noneOf メソッドを使用してオブジェクトを生成してから、要素を add しています。そこで、add メソッドを見てみると EnumSet クラスには定義されていません。EnumSet クラスは対象とする enum 型の定数の数によって生成するクラスを変化させています。64 以下であれば RegularEnumSet クラス、それ以上は JumboEnumSet クラスになります。 RegularEnumSet クラスの add メソッドは
なんと long の変数 elemens でビットフラグをあらわすようになっています。ビットの位置は enum の ordinal を利用しています。 普通に enum を使用するとパフォーマンス的に不利であるため、static final int で定義された定数を使用した場合と同じことをしているわけです。 とすると contains メソッドもこれに応じて変更されているかもしれません。
注目すべきは最後の return 文の行です。やはり long を使ったビットの AND 処理を使用しています。これで高速にフラグ処理を行えるわけですね。 先ほど enum の定数の数によりクラスを変えていることをかきましたが、その理由はビットフラグに long を使用しているからのようです。64 以下であれば 1 つの long で表すことができますが、それ以上は複数の long が必要になるからです。 そして、JumboEnumSet クラスは要素を long の配列で持つようになっています。
次に EnumMap クラスです。EnumMap クラスはキーが enum に限定されているマップです。 使い方はキーが enum だという以外は通常のマップと同じです。このクラスも enum に特化した実装が行われています。たとえば、HashMap クラスでは要素を格納するのに Entry クラスの配列を使用しますが、EnumMap クラスでは単に Object クラスの配列が使用されます。 というのは、enum を使用した場合、ハッシュ値を演算するより ordinal を使ったほうが効率がいいからです。たとえば、HashMap クラスの get メソッドは次のように実装されています。
これが EnumMap クラスの場合は
ということを行っています。 なぜこんなことができるのでしょうか。 ハッシュ値の変わりに ordinal を使用できることもそうですが、キーが enum ということは enum で定義された定数以上にマップに保持させる要素が増えないことが保証されているからです。 この 2 つのクラスは、クラスのヘッダによると Joshua Bloch が自ら書いているようです。達人のエッセンスを盗むべく、ソースを見てみるのも面白いと思いますよ。 |
|
||||
enum 型はなかなか便利です。final static な定数よりもこちらを使うほうがいろいろな面でメリットがあります。 ただプリミティブ型の定数と比べるとパフォーマンス的には不利になりますが、微々たるものだと思います。 ぜひ、その便利さを使ってみて実感してみてください。
今回使用したサンプルはここからダウンロードできます。 参考
(Dec. 2003) |
|