列挙型


前のリリースでは、列挙型を表す標準的な方法は、int Enum パターンでした。
// 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;
このパターンには次のように問題が数多くあります。 これらの問題を回避するには、型保証された列挙パターン (「Effective Java」の第 21 項を参照) を使用する方法がありますが、そのパターンにも独自の問題があります。非常に詳細であるため、エラーが起こりやすく、また列挙定数を switch 文で使用できません。

5.0 では、Java™ プログラミング言語で列挙型を言語的にサポートしました。列挙のもっとも簡単な形式では、C、C++、および C# の形式に似ています。

enum Season { WINTER, SPRING, SUMMER, FALL }

しかし、見かけに騙されることもあります。Java プログラミング言語の列挙は、その他の言語の場合に比べて非常に強力で、拡張された整数以上の機能があります。新しい enum 宣言では、完全なクラスを定義します。これは列挙型と呼ばれます。前述の問題をすべて解決するだけでなく、任意のメソッドやフィールドを列挙型に追加したり、任意のインタフェースを実装したりできます。列挙型では、すべての Object メソッドについて、品質の高い実装が可能です。Comparable かつ Serializable で、直列化形式は、列挙型の任意の変更に対応できるように設計されています。

簡単な列挙型の上に構築されたトランプのクラスの例です。Card クラスは不変であり、各 Card のインスタンスは 1 つだけ作成されます。そのため、equalshashCode をオーバーライドする必要はありません。

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
    }
}
CardtoString メソッドは、Rank および SuittoString メソッドを利用します。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 に依存しているためです。

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


Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.