【Effective Java】項目30:int 定数の代わりに enum を使用する

「第6章 enumアノテーション」に入った。

int/string enum パターン

列挙型は固定数の定数から成り立つ型です。 Javaenum 型が追加される前、列挙型を実現するためには int enum パターンと呼ばれるパターンを利用しました。

// int enum パターン
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 0;
public static final int APPLE_GRANNY_SMITH = 0;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLAE = 1;
public static final int ORANGE_BLOOD = 2;

このパターンには以下のデメリットがあります。

  • 型安全性が提供されない
  • 名前空間が提供されない
  • バイナリ互換性がない
  • グループの定数をイテレートできない
  • 表示可能な文字列に変換できない

int enum パターンと同じようなパターンに String enum パターンがあります。 このパターンは int enum のいくつかの欠点を解消しますが、一方、以下のような欠点が増えてしまいます。

  • 文字列比較になるためパフォーマンスが悪い
  • 誤字に対応できない。

enum

列挙型を利用したい場合、Java 1.5 からは enum 型利用できます。

enum 型は以下のようなコードになります。

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }

public enum Orange { NAVEL, TEMPLE, BLOOD }

表面上、この enumC/C++ などの言語のそれと似ているように見えます。 しかし、Javaenum はクラスであり C/C++ などと比較してより強力な機能を持っています。

Javaenum は以下のように捉えると分かりやすいでしょう。

public static final フィールドを通して、個々の列挙定数インスタンスにアクセスできるクラスである。 さらに、各列挙定数インスタンスはシングルトン化されている。

これに加えて、enum 型は型安全と名前空間を持ちます。 例えば、int enum パターンでは APPLE_FUJI と ORANGE_NAVEL は比較可能で実行できます。 一方、enum 型では Apple.FUJI と Orange.NOVEL は比較できません。 コンパイルエラーとなります。

さらに、enum 型では toString() が適切にオーバーライドされているため、表示可能な文字列として出力することもできます。

enum のフィールドとメソッド

さらに Javaenum 型はフィールドとメソッドを持つことが出来ます。 enum 型のフィールドとメソッドはどのような場合に役立つでしょうか。

例えば、太陽系の惑星を考えてみます。 惑星は一定数しかないため、enum 型を用いることが適切です。 さらに惑星は質量と半径をデータとして持ち、振る舞いとして表面重力を計算することができるでしょう。

これらの性質を enum で実装すると以下のようになります。

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);

    private final double mass;
    private final double radius;
    private final double surfaceGravity;

    public static final double G = 6.67300E-11;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() { return mass; }
    public double radisu() { return radius; }
    public double surfaceGravity() { return  surfaceGravity; }
    public double surfaceWeight(double mass) { return mass * surfaceGravity; }
}

enum にデータを関連付けるためには、フィールドを宣言して enumコンストラクタを書きます。

enum は一般的に不変です。 そのため、すべてのフィールドは final であるべきです。

enum の値セットは values() メソッドで取得することができます。 これらを合わせて使うと、各惑星上での重さを出力するプログラムを簡単に書くことができます。

double earthWeight = 1.0;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
    System.out.printf("Your weight on %s is %f%n",
                      p, p.surfaceWeight(mass));

enum 型は任意のインタフェースを実装することもできます。 実際、enum 型は Object の様々なメソッドを実装しています。 加えて、Comaprapble と Serializeable も実装しています。

Enum (Java Platform SE 7)

定数固有メソッド実装

ここまでの説明で enum を利用するべきほとんどのケースを利用できますが、さらに発展的な使い方も可能です。

Planet の例では定数ごとのフィールド値が異なるだけで、メソッド振る舞いは同じでした。 もし、定数ごとにメソッドの振る舞いを変えたい場合には定数固有メソッド実装を利用します。

public enum Operation {
    PLUS { double apply(double x, double y) { return x + y; } },
    MINUS { double apply(double x, double y) { return x - y; } },
    TIMES { double apply(double x, double y) { return x * y; } },
    DIVIDE { double apply(double x, double y) { return x / y; } };

    abstract double apply(double x, double y);
}

このように、Operation 内で abstract method を宣言し、各定数の宣言時に振る舞いを定義することができるのです。 これによって、新たな定数を追加したとき、振る舞いを定義し忘れることがなくなります。 もし、apply の実装を忘れた場合はコンパイルエラーになるからです。

定数固有メソッドは、定数固有データを同時に利用することができます。

public enum Operation {
    PLUS("+") { double apply(double x, double y) { return x + y; } },
    MINUS("-") { double apply(double x, double y) { return x - y; } },
    TIMES("*") { double apply(double x, double y) { return x * y; } },
    DIVIDE("/") { double apply(double x, double y) { return x / y; } };

    private final String symbol;
    Operation(String symbol) { this.symbol = symbol; }

    abstract double apply(double x, double y);
}

fromString メソッドの実装

enum はもともと、定数の名前から定数自身へ変換する valueOf(String) メソッドを持っています。

しかし、場合によっては独自の toString() を実装した方が有用な場合もあります。 そのような場合は、カスタムの文字列表現から enum に変換する fromString メソッドを作っておくべきです。

例えば先ほどの Operation クラスの toString() と fromString() は以下のように実装できます。

// 独自の toString 実装
@Override
public String toString() {
    return symbol;
}

// 独自の toString に対応する fromString()
public static Operation fromString(String symbol) {
    return stringToEnum.get(symbol);
}


// 変換を保持するマップ
private static final Map<String, Operation> stringToEnum = new HashMap<>();

static {
   for (Operation op : values()) {
       stringToEnum.put(op.toString(), op);
   }
}

ここで、strintToEnum への代入は static イニシャライザ内で行われていることに注意してください。

enumコンストラクタでは、static フィールドにアクセスすることは許されていません(コンパイル時定数フィールドを除く)。 そのため、コンストラクタで stringToEnum に値をいれようとしても、stringToEnum は null です。 これを防ぐため enumコンストラクタ内で static フィールドにアクセスしようとするとコンパイルエラーが発生します。

パフォーマンス

一般的にいえば enum は int enum パターンと比べてもパフォーマンス上の遜色はありません。

enum 型は実際にはクラスなので「ロードして初期化する」という空間と時間のコストがかかります。 ただし、enum 型の豊富な機能を考えると、資源制約の厳しいデバイス以外で int enum パターンを用いることにメリットはほとんどありません。

感想

他に「戦略 enum」を検討するという話があったのだが、enum 固有の話という感じもしなかったのでパスした。