💭

入門Javaのenum

2024/08/10に公開

この記事のサンプルコードは、enumの説明に特化しています。それゆえ、一般的には良くないとされるコードも含まれています(金額の計算で BigDecimal ではなく intdouble を使っているなど)。

分類などをどう表すか

例えば、架空のECサイトのシステムを考えます。このECサイトの会員にはブロンズ会員・シルバー会員・ゴールド会員の3つのランクがあり、ランクによって割引率などが異なります。これをどうやって表しましょうか?

まずは良くない例です。

良くない例
public class Rank {
    public static final int BRONZE = 1;
    public static final int SILVER = 2;
    public static final int GOLD = 3;
}

ランクを整数の定数で表しています。こうすると、どのような弊害があるでしょうか?

例えば、ランクを引数に取るメソッドがあるとしましょう。

ランクを引数に取るメソッドの定義の例
public class PriceCalculator {
    public int getDiscountPrice(int price, int rank) {
        switch (rank) {
            case Rank.BRONZE:
                return price;
            case Rank.SILVER:
                return (int) (price * 0.9);
            case Rank.GOLD:
                return (int) (price * 0.8);
            default:
                throw new IllegalArgumentException("Invalid rank");
        }
    }
}

このメソッドの第2引数は、本来は Rank.BRONZE などを指定してほしいのでしょう。しかし、その旨を丁寧にJavadocに書いたとしても、それを読まないでこんな風に使う人がいるかもしれません。

想定しない呼び出しの例
getDiscountPrice(10000, 1);  // 定数を使わずにハードコーディング
getDiscountPrice(10000, 0);  // 定数に定義されていない値を指定

また、ランクと割引率という非常に関連性の強い値が、別のクラスに記述されているのも気になります。後々で保守が大変そうです。

もし同じ Rank クラス内で割引率を定数化しても、ランクの定数と同じ現象(ハードコーディングなど)は起こりえます。それに、ランクにまつわる割引率以外の値が必要になったとき、更に同じような定数を増やすのは嫌ですよねえ・・・。

良くない例
public class Rank {
    public static final int BRONZE = 1;
    public static final int SILVER = 2;
    public static final int GOLD = 3;
    
    // 定数がどんどん増えていく
    public static final double DISCOUNT_RATE_BRONZE = 0.0;
    public static final double DISCOUNT_RATE_SILVER = 0.1;
    public static final double DISCOUNT_RATE_GOLD = 0.2;
}

enumとは

そこで登場するのがenumです。日本語では「列挙型」とも呼ばれます。

enumはこんなのです。

enumの例
public enum Rank {
    // 定数
    BRONZE(0.0),
    SILVER(0.1),
    GOLD(0.2);

    // フィールド
    private final double discountRate;

    // コンストラクタ
    private Rank(double discountRate) {
        this.discountRate = discountRate;
    }

    // メソッド
    public int getDiscountPrice(int price) {
        return (int) (price * (1 - discountRate));
    }
}

enumは、次の特徴を持つ特殊なクラスです。

  • フィールド・メソッドは普通のクラスと同様に定義できる
  • コンストラクタはprivateにしかできない=外部からのインスタンス生成は不可能
    • アクセス修飾子が無い場合はprivateと解釈される
    • publicprotectedを付けるとコンパイルエラー
  • クラス直下に定数を宣言する
    • これらの定数は自クラスのインスタンス
    • 自クラスのpublic static finalなフィールドとなる

つまり、BRONZESILVERGOLDRankクラスのインスタンスであり、かつRankクラスのpublic static finalなフィールドなのです。

()内に指定された値は、コンストラクタに渡す引数です。つまり今回の場合、コンストラクタを通してdiscountRateフィールドの値となります。

むりやりクラスとして書くと、こんなイメージです。

後述しますが、実際のenumとは異なります。あくまでイメージとして捉えてください。

enumを無理やりクラスで書いた例
public class Rank {
    // 定数
    public static final Rank BRONZE = new Rank(0.0);
    public static final Rank SILVER = new Rank(0.1);
    public static final Rank GOLD = new Rank(0.2);

    // フィールド
    private final double discountRate;

    // コンストラクタ
    private Rank(double discountRate) {
        this.discountRate = discountRate;
    }

    // メソッド
    public int getDiscountPrice(int price) {
        return (int) (price * (1 - discountRate));
    }
}

enumの何が嬉しいか

一言で言うと、前述のint定数の問題を解決できます。

ランクを引数に取るメソッドがあった場合、定義されていない値を指定したり、定数を使わずにハードコーディングしたりすることが不可能になります。

// Rankに定義された定数(BRONZE・SILVER・GOLD)以外は引数に指定できない!
public void doSomething(Rank rank) {
    ...
}

もちろん、nullは引数に指定できてしまいます。これはJavaの文法上、どうにもならないですね・・・😅

また、ランクと割引率(discountRate)という非常に関連性の強い値を、1つのクラスにまとめることができます。これにより、割引率を使うメソッド(getDiscountPrice())も同クラス内に定義できます。保守が楽チンですね!

更なるenumの活用術については、次の書籍を読むと良いでしょう。

[上級編]各定数ごとに処理を変えて、使う側の条件分岐を減らす

enumを使う側の良くない例
public double calc(double x, double y, Operation ope) {
    switch(ope) {
    case PLUS:
        return x + y;
    case MINUS:
        return x - y;
    case TIMES:
        return x * y;
    case DIVIDED_BY:
        return x / y;
    }
}

このような条件分岐内のロジックは、enumの各定数に持たせましょう。そうすると、使う側の条件分岐を消すことができます。

enumを使う側の良い例
public double calc(double x, double y, Operation ope) {
    return ope.eval(x, y);
}

方法① 各定数ごとにメソッドをオーバーライドする

このコードはJava Language Specificationから拝借し、少し変更を加えたものです( URL→ https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#d5e15436

enum Operation {
    PLUS {
        @Override
        double eval(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        @Override
        double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDED_BY {
        @Override
        double eval(double x, double y) {
            return x / y;
        }
    };

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

文法的には無名クラスの書き方です。

方法② インタフェースを組み合わせる

ロジックが長い場合はこちらの方がいいかもしれません。

interface Calculator {
    double eval(double x, double y);
}

class PlusCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x + y;
    }
}

class MinusCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x - y;
    }
}

class TimesCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x * y;
    }
}

class DivideCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x / y;
    }
}
enum Operation {
    PLUS(new PlusCalculator()),
    MINUS(new MinusCalculator()),
    TIMES(new TimesCalculator()),
    DIVIDED_BY(new DivideCalculator());
    
    private final Calculator calculator;

    Operation(Calculator calculator) {
        this.calculator = calculator
    }

    double eval(double x, double y) {
        return calculator.eval(x, y);
    }
}

[上級編]enumはEnumのサブクラス

enumは暗黙的にjava.lang.Enumのサブクラスとして定義されます。ということは、Enumクラスのメソッドを全て使えるということです。

Enumのメソッド一覧 → https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/Enum.html

ちなみに、型パラメータEには定義したenum自身が入ります。つまりこんな感じです。

enumをクラスで書いたイメージコード
public class Rank extends Enum<Rank> {
    ...
}

Enumで定義されたメソッドの中で、特に重要なものは次のとおりです。

メソッド 説明
String name() 定数名を返す
String toString() 定数名を返す
int ordinal() 定数が宣言された順番を返す(最初の定数が0

また、enum定義時に自動的に作成されるメソッドもあります。

メソッド 説明
static <T extends Enum<T>> T[] values() 全定数の配列を返す(順番は宣言されたとおり)
static <T extends Enum<T>> T valueOf(String name) 引数で指定された名前(完全一致)の定数を返す
利用例
Rank[] ranks = Rank.values();
Rank rank = Rank.valueOf("GOLD");  // Rank.GOLDが返る

[上級編]EnumSetの利用

java.util.EnumSetクラスは、名前の通りenumのSetです。

例えば、シルバー会員とゴールド会員のみプレゼントを受け取れるとしましょう。これを判定するメソッドはどう実装するでしょうか?

良くない例
public boolean canGetPresent(Rank rank) {
    return rank == Rank.GOLD || rank == Rank.SILVER;
}

この規模だと悪くない気もします。しかし、ランクの数が全部で10個になって、プレゼントを受け取れるランクが5個になったらどうでしょうか?書くのも読むのも辛いですね。

そんなときにEnumSetを使います。

EnumSetを使った例
import java.util.EnumSet;

public enum Rank {
    BRONZE(1.0),
    SILVER(0.9),
    GOLD(0.8);

    // フィールド等省略

    // EnumSetを利用!
    private static final EnumSet<Rank> ranksCanGetPresent = EnumSet.of(SILVER, GOLD);

    public boolean canGetPresent() {
        // EnumSet#contains()を利用
        return ranksCanGetPresent.contains(this);
    }
}
利用側のコード例
Rank rank = ...;
if (rank.canGetPresent()) {
    System.out.println("プレゼントをどうぞ!");
}

これなら、ランクの数が増えても簡単ですね!

Discussion