この記事のサンプルコードは、enumの説明に特化しています。それゆえ、一般的には良くないとされるコードも含まれています(金額の計算で
BigDecimal
ではなくint
やdouble
を使っているなど)。
分類などをどう表すか
例えば、架空の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はこんなのです。
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
と解釈される -
public
・protected
を付けるとコンパイルエラー
- アクセス修飾子が無い場合は
- クラス直下に定数を宣言する
- これらの定数は自クラスのインスタンス
- 自クラスの
public static final
なフィールドとなる
つまり、BRONZE
・SILVER
・GOLD
はRank
クラスのインスタンスであり、かつRank
クラスのpublic static final
なフィールドなのです。
()
内に指定された値は、コンストラクタに渡す引数です。つまり今回の場合、コンストラクタを通してdiscountRate
フィールドの値となります。
むりやりクラスとして書くと、こんなイメージです。
後述しますが、実際の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の活用術については、次の書籍を読むと良いでしょう。
[上級編]各定数ごとに処理を変えて、使う側の条件分岐を減らす
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の各定数に持たせましょう。そうすると、使う側の条件分岐を消すことができます。
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自身が入ります。つまりこんな感じです。
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
を使います。
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("プレゼントをどうぞ!");
}
これなら、ランクの数が増えても簡単ですね!