// int Enum Pattern - has severe problems! public static final int SEASON_WINTER = 0; public static final int SEASON_SPRING = 1; public static final int SEASON_SUMMER = 2; public static final int SEASON_FALL = 3;このパターンには次のように問題が数多くあります。
int
であるため、SEASONが必要な場所でその他のint値を渡したり、2つのSEASONを足し合わせることができる(意味がない)。SEASON_
)を接頭辞として追加し、その他のint列挙型と競合しないようにしなければならない。switch
文で使用できません。
5.0では、Java(tm)プログラミング言語で列挙型を言語的にサポートしました。列挙のもっとも簡単な形式では、C、C++、およびC#の形式に似ています。
enum Season { WINTER, SPRING, SUMMER, FALL }
しかし、見かけに騙されることもあります。Javaプログラミング言語の列挙は、その他の言語の場合に比べて非常に強力で、拡張された整数以上の機能があります。新しいenum
宣言では、完全なクラスを定義します。これは列挙型と呼ばれます。前述の問題をすべて解決するだけでなく、任意のメソッドやフィールドを列挙型に追加したり、任意のインタフェースを実装したりできます。列挙型では、すべてのObjectメソッドについて、品質の高い実装が可能です。ComparableかつSerializableで、直列化形式は、列挙型の任意の変更に対応できるように設計されています。
簡単な列挙型の上に構築されたトランプのクラスの例です。Card
クラスは不変であり、各Card
のインスタンスは1つだけ作成されます。そのため、equals
やhashCode
をオーバーライドする必要はありません。
import java.util.*; public class Card { public enum Rank { DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING, ACE } public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES } private final Rank rank; private final Suit suit; private Card(Rank rank, Suit suit) { this.rank = rank; this.suit = suit; } public Rank rank() { return rank; } public Suit suit() { return suit; } public String toString() { return rank + " of " + suit; } private static final List<Card> protoDeck = new ArrayList<Card>(); // Initialize prototype deck static { for (Suit suit : Suit.values()) for (Rank rank : Rank.values()) protoDeck.add(new Card(rank, suit)); } public static ArrayList<Card> newDeck() { return new ArrayList<Card>(protoDeck); // Return copy of prototype deck } }
Card
のtoString
メソッドは、Rank
およびSuit
のtoString
メソッドを利用します。Card
クラスのコードは短いです(約25行)。型保証された列挙(Rank
およびSuit
)を手作業で作成した場合、どちらもCard
クラス全体よりもはるかに長いものになるでしょう。
Card
のprivateのコンストラクタにはRank
およびSuit
の2つのパラメータがあります。これらのパラメータを逆にしてコンストラクタを呼び出してしまうと、コンパイラがエラーを通知します。int
列挙パターンの場合は対照的に、プログラムの実行時に失敗します。
各列挙型には、static values
メソッドがあります。このメソッドは、列挙型の値が宣言順にすべて含まれた配列を返します。このメソッドは、列挙型の値を反復するために、for-eachループと組み合わせて使用されることが一般的です。
次の例は、Card
を扱うDeal
という簡単なプログラムです。コマンド行から2つの値を読み取ります。この値はカードを配る(deal)回数(hand)と、一度に配る枚数を表しています。次に一組のカード(deck)を新しく作成し、カードを切ります。そして配られたカードの情報を出力します。
import java.util.*; public class Deal { public static void main(String args[]) { int numHands = Integer.parseInt(args[0]); int cardsPerHand = Integer.parseInt(args[1]); List<Card> deck = Card.newDeck(); Collections.shuffle(deck); for (int i=0; i < numHands; i++) System.out.println(deal(deck, cardsPerHand)); } public static ArrayList<Card> deal(List<Card> deck, int n) { int deckSize = deck.size(); List<Card> handView = deck.subList(deckSize-n, deckSize); ArrayList<Card> hand = new ArrayList<Card>(handView); handView.clear(); return hand; } } $ java Deal 4 5 [FOUR of HEARTS, NINE of DIAMONDS, QUEEN of SPADES, ACE of SPADES, NINE of SPADES] [DEUCE of HEARTS, EIGHT of SPADES, JACK of DIAMONDS, TEN of CLUBS, SEVEN of SPADES] [FIVE of HEARTS, FOUR of DIAMONDS, SIX of DIAMONDS, NINE of CLUBS, JACK of CLUBS] [SEVEN of HEARTS, SIX of CLUBS, DEUCE of DIAMONDS, THREE of SPADES, EIGHT of CLUBS]
列挙にデータと動作を追加することを考えてみます。太陽系の惑星を例にします。各惑星は、その体積と半径が判明しており、惑星面での引力と、惑星上の物質の重さを計算できます。次のようなコードになります。
public enum Planet { MERCURY (3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6), JUPITER (1.9e+27, 7.1492e7), SATURN (5.688e+26, 6.0268e7), URANUS (8.686e+25, 2.5559e7), NEPTUNE (1.024e+26, 2.4746e7), PLUTO (1.27e+22, 1.137e6); private final double mass; // in kilograms private final double radius; // in meters Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } public double mass() { return mass; } public double radius() { return radius; } // universal gravitational constant (m3 kg-1 s-2) public static final double G = 6.67300E-11; public double surfaceGravity() { return G * mass / (radius * radius); } public double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } }
列挙型Planet
にはコンストラクタがあり、各列挙定数は、作成時にコンストラクタに渡されるパラメータで宣言されています。
次のサンプル・プログラムでは、地球上での体重(任意の単位)が引数で、すべての惑星上での体重(単位は同じ)を計算して出力します。
public static void main(String[] args) { double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight/EARTH.surfaceGravity(); for (Planet p : Planet.values()) System.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass)); } $ java Planet 175 Your weight on MERCURY is 66.107583 Your weight on VENUS is 158.374842 Your weight on EARTH is 175.000000 Your weight on MARS is 66.279007 Your weight on JUPITER is 442.847567 Your weight on SATURN is 186.552719 Your weight on URANUS is 158.397260 Your weight on NEPTUNE is 199.207413 Your weight on PLUTO is 11.703031
動作を列挙定数に追加するには、もう1ステップ必要です。列挙定数ごとに、いくつかのメソッドの異なる動作を割り当てます。その方法として、列挙定数でswitchします。次の例では、列挙定数が4つの基本的な算術演算を表し、eval
メソッドが操作を実行します。
public enum Operation { PLUS, MINUS, TIMES, DIVIDE; // Do arithmetic op represented by this constant double eval(double x, double y){ switch(this) { case PLUS: return x + y; case MINUS: return x - y; case TIMES: return x * y; case DIVIDE: return x / y; } throw new AssertionError("Unknown op: " + this); } }このコードは正常に動作します。throw文なしではコンパイルされませんが、たいした問題ではありません。新しい定数をOperationに追加するたびに、新しいcaseをswitch文に追加する必要があります。追加を忘れると、evalメソッドが失敗し、前述のthrow文が実行されます。
メソッドでこれらの問題を回避するように、列挙定数ごとに異なる動作をさせる別の方法があります。メソッドを列挙型でabstractとして宣言し、定数ごとに具象メソッドでオーバーライドできます。そのようなメソッドを定数固有メソッドと呼びます。この方法を使用して、前述の例を示します。
public enum Operation { PLUS { double eval(double x, double y) { return x + y; } }, MINUS { double eval(double x, double y) { return x - y; } }, TIMES { double eval(double x, double y) { return x * y; } }, DIVIDE { double eval(double x, double y) { return x / y; } }; // Do arithmetic op represented by this constant abstract double eval(double x, double y); }
次に、Operation
クラスを実行するサンプル・プログラムを示します。2つのオペランドをコマンド行から取得して、すべての操作を反復しながら、各操作では操作を実行して結果の等式を出力します。
public static void main(String args[]) { double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); for (Operation op : Operation.values()) System.out.printf("%f %s %f = %f%n", x, op, y, op.eval(x, y)); } $ java Operation 4 2 4.000000 PLUS 2.000000 = 6.000000 4.000000 MINUS 2.000000 = 2.000000 4.000000 TIMES 2.000000 = 8.000000 4.000000 DIVIDE 2.000000 = 2.000000定数固有のメソッドはやや複雑な方法であるため、多くのプログラマは使用する必要がありませんが、知っておくと便利です。
列挙をサポートするために、2つのクラスがjava.util
に追加されました。特殊な目的を持つSet
およびMap
の実装で呼び出されるEnumSet
およびEnumMap
です。EnumSet
は、列挙用の高パフォーマンスなSet
実装です。列挙セットの全メンバーは、列挙型が同じでなければなりません。内部的には、ビット・ベクトルで表されます。ビット・ベクトルは通常、単一のlong
値です。EnumSetでは、列挙型の範囲を反復できます。たとえば次の列挙宣言を考えてみます。
enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }曜日を反復できます。
EnumSet
クラスは、簡単に反復処理を行うためのstaticファクトリを提供します。
for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY)) System.out.println(d);EnumSetは、従来のビット・フラグのための、さまざまな型保証された置換も提供します。
EnumSet.of(Style.BOLD, Style.ITALIC)
同様にEnumMap
は、列挙キーで使用する高パフォーマンスなMap
実装です。内部的には配列として実装されます。EnumMapは、Map
インタフェースの豊かさや安全さと、配列の高速なアプローチとが結び付いたものです。列挙を値にマップする場合は、配列ではなく、常にEnumMapを使用してください。
前述のCard
クラスには、deckを返すstaticファクトリが含まれています。しかし、rankとsuitから個別のカードを取得する方法はありません。単にコンストラクタを公開すると、シングルトン属性(各カードのインスタンスは1つしか存在できない)が壊れてしまいます。シングルトン属性を維持するstaticファクトリの記述例を示します。入れ子にされたEnumMapを使用します。
private static Map<Suit, Map<Rank, Card>> table = new EnumMap<Suit, Map<Rank, Card>>(Suit.class); static { for (Suit suit : Suit.values()) { Map<Rank, Card> suitTable = new EnumMap<Rank, Card>(Rank.class); for (Rank rank : Rank.values()) suitTable.put(rank, new Card(rank, suit)); table.put(suit, suitTable); } } public static Card valueOf(Rank rank, Suit suit) { return table.get(suit).get(rank); }
EnumMap
(table
)は、各rankをカードにマップするEnumMap
に、各suitをマップします。valueOf
メソッドによる検索は、内部的には2回の配列アクセスで実装されていますが、コードはわかりやすくて安全です。シングルトン属性を維持するには、Card
内のプロトタイプdeckの初期化におけるコンストラクタの呼出しを、次の新しいstaticファクトリの呼出しで置換することが必須です。
// Initialize prototype deck static { for (Suit suit : Suit.values()) for (Rank rank : Rank.values()) protoDeck.add(Card.valueOf(rank, suit)); }また、
table
の初期化は、protoDeckの初期化よりも先に行わなければなりません。protoDeckはtableに依存しているためです。
それでは列挙はいつ使用すべきでしょうか。列挙は、定数の固定セットが必要な場合は何回でも使用できます。自然に列挙される型(惑星、曜日、トランプのマークなど)だけでなく、メニューの選択項目、丸めモード、コマンド行フラグなど、可能な値すべてがコンパイル時にわかっているセットにも使用できます。列挙型の定数セットは常に固定されている必要はありません。この機能は、列挙型をバイナリ互換で展開できるように設計されました。