int enumパターンは手軽で便利な方法ですが、問題となることがあります。
例えば、次のコードはCalendar.FEBRUARYの値が2だと勘違いして書いてしまったコードですが、int型の値を比較しているので、コンパイルエラーにはなりません。しかし、実行すると「FEBRUARY」ではなく、「JANUARY」と表示されるので、「プログラムのどこかで間違ってしまったのだ」と気がつきます。
package sample21; import java.util.Calendar; public class PrintFebruary { static void printMonth(int month) { if (month == 1) { System.out.println("JANUARY"); } if (month == 2) { System.out.println("FEBRUARY"); } } public static void main(String[] args) { printMonth(Calendar.FEBRUARY); } }
JANUARY
もちろん、このプログラムでは比較するときに、「month == 1」ではなく、「month == Calendar.JANUARY」と書くべきなのですが、このように数値を書いてしまうことはありえます。
また、定数変数で比較する場合でも、「month == Calendar.SEPTEMBER」と書くつもりだったのが、「month == Caldendar.SECOND」としてしまうこともありえます。Eclipseなどのコード補完機能を使っていると、「SE」まで入力して、残りを確認しないまま書いてしまうこともあるからです。
この場合、どちらもint型であるため、コンパイルエラーとなりませんから、実際にプログラムを動かしてみない限り、挙動がおかしいことに気がつきません。小規模なプログラムを書いているうちは、「こんな単純ミスは発生しない」と思うかもしれませんが、実際のコーディングでは似たようなことがよく発生します。
こういった問題点は、数値を使って各月を表現しているために発生しています。各月を表現するデータ型を定義すれば、解決しそうです。
「タイプセーフenumパターン」といわれるクラスを使った列挙のためのパターンを使うことにより、Javaでは「int enumパターン」よりも、型について安全(タイプセーフ)な列挙が可能となります。
最新のJavaは、後述する列挙型が使えるので、タイプセーフenumパターンを使う機会は少ないかもしれませんが、「列挙型が使えない場合には知っておいた方が良い」のと、「列挙型を理解するうえでも、これを知っておいた方が良い」ということで、先に説明しておきます。なお、ここでは基本概念の理解を目的とするので、必要最低限の説明にしておきます。
Javaでは、独自のデータ型を定義するには、クラスを宣言すればいいので、各月を表現するクラスやオブジェクトを考えることになります。タイプセーフenumパターンでは、次のようにコンストラクタをprivateとして、extendsできないようにしています。
package sample21; public class TypesafeEnumMonth { private final String name; private TypesafeEnumMonth(String name) { this.name = name; } public String toString() { return name; } public static final TypesafeEnumMonth JANUARY = new TypesafeEnumMonth("JANUARY"); public static final TypesafeEnumMonth FEBRUARY = new TypesafeEnumMonth("FEBRUARY"); public static final TypesafeEnumMonth MARCH = new TypesafeEnumMonth("MARCH"); private static final TypesafeEnumMonth[] PRIVATE_VALUES = { JANUARY, FEBRUARY, MARCH, }; public static TypesafeEnumMonth[] values() { return PRIVATE_VALUES; } }
月の名前を保持するnameフィールドはfinalで修飾しています。そして、各月を表すために、TypesafeEnumMonth型のfinalでstaticなフィールドを用意して、「JANUARY、FEBRUARY」といった名前を付けています。これらがTypesafeEnumMonth型であり、しかも名前も分かりやすい定数変数として用意されている点に注目してください。
なお、このフィールドは定数として利用するので、大文字の名前とするのが一般的です。また、ここでは、簡単のためにMARCHまでしか用意していませんが、本来は同様にしてDECEMBERまで用意します。
さて、TypesafeEnumMonthクラスと、定数フィールドを使うためには、次のTypesafeEnumMonthSampleクラスのようにします。このクラスでは、列挙する値の一覧をMonthクラスのvaluesメソッドで取得して表示しています。
また、Month.JANUARYと一致するものを判定しています。
package sample21; public class TypesafeEnumMonthList { public static void main(String[] args) { TypesafeEnumMonth[] ms = TypesafeEnumMonth.values(); for (TypesafeEnumMonth m : ms) { System.out.println(m); } for (TypesafeEnumMonth m : ms) { if (m == TypesafeEnumMonth.JANUARY) { System.out.println("m equals " + m); } } } }
JANUARY FEBRUARY MARCH m equals JANUARY
このプログラムを見れば分かるように、TypesafeEnumMonthのようなクラスを用意することで、型について安全な列挙が可能なプログラムを用意できます。TypesafeEnumMonth.JANUARYはTypesafeEnumMonth型なので、TypesafeEnumMonth型以外の値、例えば文字列型の値と比較できません。
int enumパターンでは、Calendar.SEPTEMBERとCalendar.SECONDを比較してもエラーになりませんが、タイプセーフenumパターンでは、SEPTEMBERはTypesafeEnumMonth型で用意することになり、SECONDは別の型で用意することになるので、比較しようとするとエラーとなります。便利ですよね。
ところが、「何か列挙用の値を使うたびに、こういったクラスを最初から作成して用意するのは大変だ」と思うかもしれません。 そこで、Javaではこういったタイプセーフな列挙が簡単にできるように、「enum」というキーワードが用意されています。
前置きが長くなりましたが、いよいよenumについて説明します。Javaには列挙を簡単に表現するために「enum」というキーワードが用意されています。これを使って宣言される型は、「列挙型(enumerated type)」と呼ばれ、参照型に含まれます。enumというキーワードを使うため、「enum型(enum type)」とも呼ばれます。
例えば、季節を列挙型で宣言すると、次のようになります。enumは、特殊な種類のクラスです。
package sample21; public enum Season { SPRING, SUMMER, FALL, WINTER }
列挙型の基本的な宣言は次のようになります。{ }で囲まれている部分は本体です。本体には「列挙定数(enum constants)」を単純に列挙します。
クラス修飾子 enum 識別子 { 列挙定数, 列挙定数, ... }
列挙定数とは、「enum定数」ともいわれ、Seasonの例を見て分かるように、SPRINGやSUMMERといった値のことです。大文字で書きます。
なお、列挙定数の最後の要素に対して「,」を付けることが可能なので、Season型の本体は「 { SPRING, SUMMER, FALL, WINTER, }」のように記述することもできます。
クラス修飾子にはabstract、finalは指定できません。アクセス修飾子(publicやprivateなど)、staticなどが指定できます。厳密には他にもありますが、ここではこれだけ覚えておけば十分です。
列挙型では、まず列挙定数をすべて記述しておく必要があります。列挙型は特殊な種類のクラスなので、フィールドやメソッドも宣言できます。これらを宣言する場合は、列挙定数の最後に「;」を付けて終了させてから、その後に追加する必要があります。
ちょっとした用途で使う分には以上のことを覚えておけばいいのですが、本格的に使いたい場合には他にも覚えておいた方が良いことがあるので、説明しておきます。
列挙型はクラスですから、コンストラクタがありますが、次の3つの制約があります。コンストラクタで処理できない初期化処理はstatic初期化ブロックを使うことになります。
他にも列挙型には、いくつか特殊な性質や制約があります。
まず、暗黙のうちに、「java.lang.Enum」というクラスをextendsし、「java.io.Serializable」「java.lang.Comparable」というインターフェイスを実装しています。
次に、コンストラクタがprivateとなるので、列挙型をextendsできません。従って、finalクラスであるかのように振る舞うことになります。本記事で詳細に説明しませんが、列挙型の機能を追加するには、インターフェイスを実装します。その場合は、識別子と本体の間に「implements インターフェイス名」のように実装するインターフェイスを記述し、メソッドを実装します。
また、列挙定数は暗黙のうちに、それを宣言している列挙型と同じ型のstaticフィールドとなります。
最後に、finalizeメソッド(java.lang.Objectクラスで宣言されているメソッド)をオーバーロードすることもできませんメソッド(java.lang.Objectクラスで宣言されているメソッド)をオーバーロードすることもできません。
列挙型はジェネリックスの機能を利用して、自分自身の型を使った2つのstaticメソッドを持っています。このメソッドでは、列挙型Eがあるとすると、次のように自分自身の型Eが型変数として指定されています。
型変数については、連載第19回の「キュー構造をJavaで実装してジェネリック型を理解する」で解説した「ジェネリック型を使うための基礎知識」で再確認してください。
public static E[] values(); public static E valueOf(String name);
valuesメソッドは、宣言された順序で各列挙定数を含む配列を返します。valuesOfメソッドは、指定された名前を持つ列挙定数を返します。Seasonの例において、「Season.valueOf("SPRING")」と指定すると、列挙定数SPRINGを返します。
なおtoStringメソッドは、列挙定数名を返します。つまり、「Season.SPRING.toString()」は「"SPRING"」という文字列を返します。
列挙型について理解したところで、サンプルプログラムを作成してみましょう。ここでは、「ネストした列挙型」としてMonthを宣言しています。このプログラムでは、Monthに含まれる列挙定数を表示し、Month.JANUARYを判定しています。
package sample21; public class EnumMonthList { enum Month { JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER, } public void exec() { for (Month m : Month.values()) { System.out.println(m.toString()); } for (Month m : Month.values()) { if (m == Month.JANUARY) { System.out.println("m == " + m); } } } public static void main(String[] args) { EnumMonthList app = new EnumMonthList(); app.exec(); } }
JANUARY FEBRUARY MARCH APRIL MAY JUNE JULY AUGUST SEPTEMBER OCTOBER NOVEMBER DECEMBER m == JANUARY
この例で分かるように、Month.JANUARYといった列挙定数との比較に「==」が使えます。
また、「"m == " + m」の変数mでは、実際には「Month.JANUARY.toString()」のように、toStringメソッドが呼ばれています。これは、列挙定数に対応する文字列を返しますから、この結果は「"JANUARY"」のようになります。
次ページでは、さらに列挙型について解説し、「java.utilEnumMap」「java.util.EnumSet」クラスの使い方を説明します。
Copyright © ITmedia, Inc. All Rights Reserved.