列挙型


前のリリースでは、列挙型を表す標準的な方法は、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(tm)プログラミング言語で列挙型を言語的にサポートしました。列挙のもっとも簡単な形式では、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, 2020, Oracle and/or its affiliates. All rights reserved.