// 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™ プログラミング言語で列挙型を言語的にサポートしました。列挙のもっとも簡単な形式では、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 に依存しているためです。
列挙は、定数の固定セットが必要な場合は何回でも使用できます。自然に列挙される型 (惑星、曜日、トランプのマークなど) だけでなく、メニューの選択項目、丸めモード、コマンド行フラグなど、可能な値すべてがコンパイル時にわかっているセットにも使用できます。列挙型の定数セットは常に固定されている必要はありません。この機能は、列挙型をバイナリ互換で展開できるように設計されました。