33
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

入門Javaのenum

この記事のサンプルコードは、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("プレゼントをどうぞ!");
}

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
33
Help us understand the problem. What are the problem?