列挙型

J2SE 5.0から列挙型が追加されました。列挙型は特殊なクラスで java.lang.Enumクラスのサブクラスとして実現されます。クラスとして実現されているため、型安全であり フィールドやメソッドを追加して機能を拡充することができます。

 

列挙型の定義と利用

列挙型の定義

列挙型はキーワード「enum」を使って定義します。

 

上の例ではOne, Two, Threeがそれぞれ列挙子です。列挙子には「public static final」が暗黙に付けられます。命名則としては定数と同様に大文字のsnake_caseとする場合と、クラスなどと同様にCamelCaseにする場合が見受けられます。

 

列挙型の利用

列挙子は「列挙名.列挙子」の形で参照しますが、switch文のcase節では列挙子のみで参照することができます。また、staticインポートによって「列挙名」を省略することもできます。

 

列挙型の特徴、メリット、制限など

列挙型の特徴やメリット、制限などを簡単にまとめます。

  • 各列挙子はpublic static finalなフィールドとして実現されています。
  • 列挙型からサブクラスを作成することはできません。
  • publicなコンストラクタを持つことはできません。
  • 列挙型の変数がnullでなければ、定義した列挙子のいずれかを参照していることが保証されます。
  • 列挙型ごとに異なる名前空間を持ちます。そのため、異なる列挙型で同じ名前の列挙子を定義しても問題ありません。
  • 任意のフィールドやメソッドを追加することができます。また、任意のインタフェースを実装することもできます。
  • Object由来のメソッド(equals()やhashCode()など)を実装していて、ComparableやSerializableインタフェースを実装しています。

 

列挙型のメソッドとメンバの追加

Javaの列挙型は CやC++と異なり、単純な定数の集まりではなく 実体はクラスです。列挙型は特殊なクラスで、列挙子は不変オブジェクトで実現されています。列挙型のスーパークラスになる抽象クラスjava.lang.Enumが用意されていて、キーワード「enum」で宣言した列挙型はEnumクラスのサブクラスとして実現されます。そのため 通常のクラスと同じようにフィールドやメソッドも持っていますし、任意のフィールドやメソッドを追加したりオーバーライドすることもできます。更に、任意のインタフェースを実装することもできます。

 

列挙型のメソッド

列挙型はjava.lang.Enumクラスのサブクラスとして実現されます。列挙子は列挙型のクラスフィールドとして実現されていて、コンストラクタをprivateにすることにより 他のクラスからインスタンスを生成できないようにしています。

列挙型は主に次のようなメソッドを持っています。(この他にも Object由来のメソッドやComparableのメソッドも実装しています。)

列挙型の主なメソッド
メソッド 概要
String name() 列挙子の名前を返します。
int ordinal() 列挙子の序数(0オリジンの連番)を返します。
String toString() name()と同じものを返します。オーバーライド可能です。
列挙名[] values() 列挙子の配列を返します。

Enumクラスではname(名前)とordinal(序数)というfinalフィールドを持っています。列挙子のインスタンスを生成する際にEnumクラスのコンストラクタに列挙子の名前と序数が渡されてフィールドが初期化されます。

尚、CやC++のように 列挙子の序数を指定することはできず、次のようになります。

  • 序数は0オリジンの連番となり、初期値を指定することはできません。
  • 序数の番号を飛ばしたり、重複した序数を指定するようなことはできません。
  • 列挙子の追加・削除を行うとそれに伴って序数も変更されます。そのため、純粋に列挙子のインデックスが必要な箇所以外では 序数に依存した処理は避けるべきです。

 

コンストラクタ・フィールド・メソッドの追加

列挙型はEnumのサブクラスであり、任意のコンストラクタ・フィールド・メソッドを追加することができます。また、上の表の中で触れたようにtoString()をオーバーライドすることができます。(name()やordinal()など他のメソッドはfinal修飾子が指定されているためオーバーライドすることはできません。)

列挙型でコンストラクタを追加した場合 暗黙的にスーパークラスのEnumのコンストラクタが呼ばれてnameとordinalが初期化されます。そのためコンストラクタを追加していない場合と同様に name()やordinal()を利用することができます。

列挙子は不変オブジェクトであるため、フィールドを追加する場合は基本的にはfinalにします。

列挙型クラスの初期化では、列挙子のインスタンスが生成されてからクラスフィールドが初期化されます。そのため、列挙型のコンストラクタの中ではクラスフィールドにアクセスすることはできません。(コンパイルエラーになります。)逆に クラスフィールドの初期化やstaticイニシャライザの中では 初期化された列挙子にアクセスすることができます。

 

列挙子固有のメソッド実装

列挙型では 列挙子ごとにメソッドの振る舞いを定義することができます。これにより 列挙子をStrategyパターンの各Strategyとすることができます。列挙子ごとにメソッドを実装する場合は次のように定義します。

 

列挙子固有クラス本体を記述することによって、匿名クラスが作成されます。列挙子固有クラス本体を定義しない列挙子は列挙型のインスタンスになり、列挙子固有クラス本体を定義した列挙子は列挙型の匿名サブクラスのインスタンスになります。匿名クラスで列挙型のメソッドをオーバーライドすることにより、列挙子ごとにメソッドの振る舞いを定義できる仕組みになっています。

そのため、列挙子固有クラス本体は 匿名クラスと同じように書くことができ、同じ制限を受けます。任意のフィールドやメソッドを定義することができますが、finalフィールドを除くstaticなメンバを持つことはできません。

 

列挙型BitOperationではapply()を抽象メソッドにしています。これによって、新たな列挙子を追加した場合に apply()の実装が強制され apply()の実装漏れを防ぐことができます

 

列挙型にまつわるライブラリ

Java標準ライブラリには、列挙型にまつわる便利なクラスが用意されています。java.util.EnumSetクラスはビットフィールドを表現することができます。また、java.util.EnumMapクラスは列挙子をキーとするMapの実装です。

ビットフィールドを表現するEnumSet

ビットフィールドを表現したい場合には EnumSetを利用することができます。EnumSet自体は抽象クラスでstaticなファクトリメソッド of()、allOf()、noneOf()を通してEnumSetのインスタンスを生成します。staticファクトリメソッドに列挙型のクラスを指定することにより、その列挙型の列挙子を要素に持つSetを作成して返します。列挙子の数が64以下であれば 内部的にはlong値のビットフィールドそのものになるため、ビットフィールドを実装する場合に比べてパフォーマンスに遜色はありません。

EnumSetの生成例は次のようになります。

EnumSetはSetインタフェースを実装しているため、EnumSetを受け取った側はSetインタフェースのメソッドを通して指定された列挙子を取得することができます。

 

 

列挙子をキーとするMapの実装:EnumMap

EnumMapは列挙子をキーとするMapの実装です。列挙型のフィールドを持つクラスのオブジェクトを列挙子ごとにグルーピングしたMapが必要な場合や、列挙子のマトリクスを表現するMapが必要な場合に利用することができます。EnumMapは内部的に配列を使っているので、配列で実現した場合と比べてパフォーマンスに遜色はありません。

列挙型のフィールドを持つクラスのオブジェクトの配列から 列挙子ごとにグルーピングしたMapに変換する例を挙げます。フルーツを表すクラスのオブジェクトを色ごとにグルーピングします。Mapへの変換方法には次の2通りがあり、どちらも一長一短があります。

  • Streamを用いてMapの生成とMapへの格納を同時に行う方法
    未使用の列挙子に対するエントリがMapに格納されません。そのため、使用量の面では効率的ではありますが、Map.get()で未使用の列挙子を指定するとnullが返るためnullに対する処理が必要になります。
  • 列挙子を走査して予めMapを作成した後に Mapへの格納を行う方法
    未使用の列挙子に対するエントリもMapに格納されます。そのため、使用量の面では非効率になりますが、Map.get()で未使用の列挙子を指定した場合にも空のSetが返るため 安全に利用できます。

groupingBy()やtoSet()はjava.util.Collectorsのクラスメソッドで、汎用的な終端操作を提供してくれます。詳しくは「ストリーム」の章の「終端操作」を参照してください。

実行結果は次のようになります。

未使用の列挙子BLUEに対するエントリが、前者では格納されず 後者では格納されている様子が分かります。

 

続いて列挙子のマトリックスを表現するMapを作成する例を挙げます。開発元と種類に応じたオフィススイートの製品のマトリックスを作成します。同様に製品の配列をMapに変換するには2通りの方法があります。先の例では列挙子が未使用の場合は空のSetを格納しましたが、こちらの例では列挙子が未使用の場合は空のOptionalを格納しています。

実行結果は次のようになります。

“IBM”に対するエントリが、前者では格納されず 後者では格納されている様子が分かります。また、”Google”の”Graphics”に対するエントリが、前者では格納されず 後者では格納されている様子が分かります。

 

列挙子の拡張

列挙型は派生クラスを作成することができません複数の列挙型を同じ型として扱いたい場合は スーパータイプとなるインタフェースを定義します。列挙子を複数の列挙型に分けたい場合や、ユーザに列挙子追加の拡張性を提供する場合などに このパターンが有効です。

メソッドの引数や戻り値を 列挙型のスーパータイプのインタフェースのコレクションや配列とします。これによって 異なる列挙型の列挙子を混在させて受け渡すことができます。渡す側は 任意の列挙子をコレクションや配列に詰め込んで渡し、受け取る側ではコレクションや配列を走査して列挙子に応じた処理を行います。

上の例ではCarMakerインタフェースを通してJpnCarMakerとUsCarMakerの列挙子を同じ型として扱っています。CarMakerを実装した列挙型を定義することによって、列挙子追加の拡張性を提供することができます

 

 

また、任意の列挙子を渡すのではなく、列挙型のクラスを渡して 受け取り側で列挙子を列挙することもできます。

ここで渡しているCarMakerインタフェースのクラスリテラル(JpnCarMaker.class)は境界型トークン(bounded type token)と呼ばれます。また、受け取り側では 型Tが列挙型でありCarMakerのサブタイプであると定義しています(T extends Enum<T> & CarMaker)。型Tが列挙型であることから getEnumConstants()がnullでないことを保証しています。また、型TがCarMakerのサブタイプであることから getEnumConstants()の戻り値を キャストなしでCarMakerの変数に格納することができます。

 

 

 

タイトルとURLをコピーしました