Effective Javaを読むチャレンジ-項目30 その3-

enumはクラス

enumはクラスなので、アクセス可能性を持っています。そして他のクラス同様、項目13のアクセス可能性を最小限にするという考え方が当てはまります。また、基本的にenumはトップレベルのクラスであるべきで、enumの使用が特定のトップレベルのクラスに結びついているならば、そのクラスのメンバークラスにするべきとあります。

その例として、java.math.RoundingMode enumについて書かれています。RoundingModeは小数に対する丸めモードを表す定数として使われますが、もともと丸めモードを表す定数はBigDecimalクラスに定義されているint定数でした。それらのint定数をトップレベルのRoundingMode enumとして新たに追加するにあたり、元々使われていたint定数から新しいRoundingMode enumの定数を返すvalueOfメソッドがRoundingModeに用意されています。こういう互換性のための手法は勉強になりますねえ・・・本書とは関係ない話ですが。

各定数ごとに異なる振る舞いを持たせる方法

まず、本書の惑星に関するenumのサンプルです。

enum定数にデータを関連付けるためには、まずインスタンスフィールド(enumは不変なのでfinalであるべき)を宣言して、データを受け取るコンストラクタを書き、データを保持するようにします。また、振る舞いを関連付けるためにはenum内に単にメソッドを定義します。

単にenum内にメソッドを定義すると、enum定数はすべて同じ振る舞いを持ちます。ですが、各定数で異なる振る舞いを持たせたい場合もあるでしょう。その場合は本書のサンプルのようにenum内のメソッドで「switch(this){各定数のcase}」を行うのではなく、以下のように書くことができます。

最初に、enum内に抽象メソッドを定義します。そして次に定数の後ろの{}のなかでその抽象メソッドの実装を定義しています。定数の後の{}は定数固有のクラス定義を意味し、定数固有クラス本体(constant-specific class body)といいます。また、定数固有クラス本体のなかで定義されているメソッドを定数固有メソッド実装(constant-specific class body)と呼びます。

その定数固有クラスとは何でしょうか。このOperation enumをコンパイルすると、以下のようなクラスファイルができます。

ちなみに、Planet enumをコンパイルしてもPlanet.classの1ファイルしか生成されません。この新しくできたOperation$1.classというファイルは何なのでしょうか、逆コンパイルしてみます。以下はオプションなしのjavapコマンドで表示しています。

定数固有クラス本体はenumを継承したクラスということですね。今度はOperation enumを逆コンパイルしてみます。すると、例えばPLUS定数はOperation$1のインスタンスであることが分かります。定数固有のクラスであるということが分かりました。

このパターンの利点は、定数の追加によって発生する、applyメソッドの実装漏れを防ぐことができる点です。また、定数をenumに追加したけど定数固有メソッド実装を書かなかった場合にはコンパイルエラーになります。それはenum側で定義したメソッドが抽象メソッドだからです。enumを継承する定数固有クラスにおいて、それがエラーになるのは明白ですね。

Operation enumに定数固有データを追加した次のサンプルについては、難しいところはありません。ポイントは、toStringメソッドをオーバーライドするとenumの有用性が増す場合があるということですね。symbolを返すメソッドを作ってもいいですが、本書の例の方がクライアントコードがずっと読みやすくなります。

この次の話ですが、EnumクラスのvalueOfメソッドはenum型のClassオブジェクトと定数の名前から該当するenumを返すメソッドです。Javaソースではenum型のデシリアライズなどで使われています。このとき、valueOfのように文字列からenumを返したい場合があるならfromStringメソッドを検討してくださいという内容です。

fromStringメソッドを含めたOperation enumは最終的にこんな感じになります。

fromStringメソッドを作成する場合、カスタム文字列は一意でなくてはなりません。上記のように、fromStringメソッドの実装において内部で保持するMapのキーにカスタム文字列を使用するからです。

そして次の説明がちょっと難しいです。カスタム文字列とenumを対応させるためのMapを、static初期化子(イニシャライザ)のなかで生成しています。これをコンストラクタ内からMapの生成を行うように変更すると、コンパイルエラーになるというのです。実際やってみるとこうなります。

「エラー: 初期化子からstaticフィールドへの参照が不正です」というコンパイルエラーが発生します。eclipseでは「イニシャライザーの中では、静的な列挙フィールド Operation.stringToEnum は参照できません」というエラーが出ます。

enumのコンストラクタは、コンパイル時定数フィールドを除いて、enumのstaticのフィールドにアクセスできない

これはどういうことかというと、上記のコードがコンパイルエラーにならないとすると、enum定数のコンストラクタを実行した時点で、stringToEnumのMapインスタンスが生成されていないため、NullPointerExceptionがスローされてしまいます。その例外を未然に防ぐためのコンパイルエラーということです。

すなわち、enum型の定数以外のstaticなフィールドは、enum型のコンストラクタが実行された時点ではまだ初期化されていないということです。

このことを確かめてみます。Apple enumをenumではなくタイプセーフenumパターンで書いてみます。

このコードはenumとは違い、コンパイルされます。実行すると、java.lang.ExceptionInInitializerErrorがスローされます。実行結果を見てみます。

定数Aは、Appleクラスのコンストラクタ内で参照できました。しかし、staticフィールドである変数Bは初期化されず、intの初期値である0が表示されています。そして、同様に定数(リテラルで初期化しているstatic finalフィールドのこと)ではないstatic finalフィールドであるApple.FUJIのインスタンスがnullになっています。これで定数以外のstaticフィールドがAppleクラスのコンストラクタ実行時に初期化されていないことが分かります。そして本書で書かれているとおり、NullPointerExceptionがスローされています。

今回はここまで。次は戦略enumパターンの部分を読んでいきます。

広告
  • LINEで送る