Java : 列挙型(enum)の基本

列挙型は、複数の異なる定数を1つの集まりとして宣言できます。
例えば DayOfWeek(曜日) という列挙型を宣言して、その列挙型に MONDAY(月曜日)、TUESDAY(火曜日) ... SUNDAY(日曜日) と定数を定義します。

本記事では、そんな列挙型の基本的な使い方をご紹介します。


概要

8.9. Enum Classes
An enum declaration specifies a new enum class, a restricted kind of class that defines a small set of named class instances.

列挙型(クラス) は、Java言語仕様に組み込まれている機能です。
複数の異なる定数を1つの集まり(集合)として宣言することができます。

列挙型で定義された定数のことを、本記事では公式の翻訳にならって 列挙型定数 と表記します。
他のプログラミング言語では、識別子、列挙子ともいわれています。

簡単な例

さっそくコード例を見ていきましょう。
月曜日、火曜日、水曜日…という曜日を、列挙型で宣言してみます。

enum DayOfWeek {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY,
}

enum キーワードを使い、その後に列挙型の名前がきます。
今回、列挙型の名前は DayOfWeek (曜日)としました。

そして、その曜日という集まりに、

  • MONDAY (月曜日)
  • TUESDAY (火曜日)
  • ...
  • SUNDAY (日曜日)

という列挙型定数が定義されていきます。

それでは、実際にこの列挙型を使ってみましょう。

void main() {
    func(DayOfWeek.SUNDAY); // 今日は...週末!
    func(DayOfWeek.WEDNESDAY); // 今日は...平日!
}

void func(DayOfWeek dayOfWeek) {

    System.out.print("今日は...");

    if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) {
        System.out.println("週末!");
    } else {
        System.out.println("平日!");
    }
}

列挙型定数を使うには、

  • 列挙型名 + . (ピリオド) + 列挙型定数名

とします。new は必要ありません。


補足

  • 標準APIには、今回例とした列挙型と同名のDayOfWeek があります。
    ただし、便利なメソッドが追加されていたりと、少し拡張されたものとなっています。

列挙型の仕様

列挙型定数の型

enum DayOfWeek {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY,
}

列挙型定数のは、列挙型そのものとなります。

final DayOfWeek monday = DayOfWeek.MONDAY;
System.out.println(monday); // MONDAY

DayOfWeek.MONDAY (列挙型定数) の型は DayOfWeek になるわけですね。

列挙型の変数には、列挙型定数と null だけが代入できます。
数値や文字列は代入できません。

final DayOfWeek aaa = null; // OK

final DayOfWeek bbb = 0; // コンパイルエラー
final DayOfWeek ccc = "MONDAY"; // コンパイルエラー

本質的にはクラス

これは、すべてのJava言語列挙型クラスの共通ベース・クラスです。

enum で宣言された列挙型は、上記のEnumクラスを暗黙的に継承します。
つまり、列挙型は本質的にはクラスでありObjectでもあります。

例えば、

enum Sample {
    AAA, BBB, CCC,
}

と宣言された列挙型は、暗黙的に次のように宣言されます。

enum Sample extends Enum<Sample> {
    AAA, BBB, CCC,
}

よって、列挙型定数は Enumクラスの各種メソッドが使えます。

// ordinal メソッドは、列挙型定数が定義された順番(序数)を取得します。
// 序数は 0 から採番されます。
System.out.println(Sample.AAA.ordinal()); // 0
System.out.println(Sample.BBB.ordinal()); // 1
System.out.println(Sample.CCC.ordinal()); // 2

// toString メソッドは、列挙型定数名を文字列として取得します。
System.out.println(Sample.AAA.toString()); // AAA
System.out.println(Sample.BBB.toString()); // BBB
System.out.println(Sample.CCC.toString()); // CCC

それ以外の API 使用例は、下記の記事もご参照ください。


さらに、列挙型には、Enumクラスでは宣言されていない2つのメソッドが暗黙的に宣言されます。

  • public static T valueOf (String name) : 指定した名前の列挙型定数を取得する
  • public static T[] values () : すべての列挙型定数を取得する

イメージとしては次のような宣言となります。

enum Sample extends Enum<Sample> {
    AAA, BBB, CCC;

    public static Sample valueOf(String name) {
        ...
    }
    public static Sample[] values() {
        ...
    }
}

暗黙的に宣言されたメソッドを使う例になります。

final Sample aaa = Sample.valueOf("AAA");
System.out.println(aaa); // AAA

final Sample[] values = Sample.values();
System.out.println(Arrays.toString(values)); // [AAA, BBB, CCC]

注意点として、暗黙的に宣言される内容を、明示的に宣言しようとするとコンパイルエラーになります。
あくまで暗黙的に宣言されるのが前提となります。

// ソースコード上で次のように書くと、コンパイルエラーとなります。
enum Sample extends Enum<Sample> {
    AAA, BBB, CCC,
}

列挙型定数のインスタンスは常に1つ

列挙型定数のインスタンスは常に1つであることが、Java言語仕様で保証されています。
そのため、列挙型定数の比較には equals メソッドではなく、== 演算子が使えます。

System.out.println(Sample.AAA == Sample.AAA); // true
System.out.println(Sample.AAA == Sample.BBB); // false
System.out.println(Sample.AAA == Sample.CCC); // false

インスタンスが1つであることが保証できなくなる操作はできません。

// 複製や、新しいインスタンスを生成することはできません。
final Sample clonedAAA = Sample.AAA.clone(); // コンパイルエラー
final Sample newAAA = new Sample("AAA", 0); // コンパイルエラー

switch式では列挙型定数名だけでアクセス可能

通常、列挙型定数にアクセスするには、

  • 列挙型名 + . (ピリオド) + 列挙型定数名

とします。(if文の判定で使う場合など)

enum Sample {
    AAA, BBB, CCC,
}

void func1(Sample sample) {

    if (sample == Sample.AAA || sample == Sample.BBB) {
        System.out.println("AAA or BBB");
    } else {
        System.out.println("それ以外!");
    }
}

switch式 (もしくはswitch文) では、列挙型名は省略してアクセスできます。

void func2(Sample sample) {

    switch (sample) {
        case AAA, BBB -> System.out.println("AAA or BBB");
        default -> System.out.println("それ以外!");
    }
}

列挙型の拡張

enum は本質的にクラスであるため、クラスと同様の拡張ができます。

それでは、具体的なコード例で見ていきましょう。

enum Bill {
    YEN_1000,
    YEN_5000,
    YEN_10000,
}

例として、Bill (紙幣)という列挙型を宣言しました。
列挙型定数として

  • YEN_1000 (千円札)
  • YEN_5000 (五千円札)
  • YEN_10000 (一万円札)

を定義しています。

単純に紙幣の種類だけを区別したいなら、これだけでいいかもしれません。
しかし、例えば紙幣の円の合計を計算したい、というときに少しだけ不便かもしれません。

まずは列挙型を拡張しない例を見てみましょう。

// listにある紙幣の円の合計を算出したいとします。
final var list = new ArrayList<Bill>();

list.add(Bill.YEN_1000);
list.add(Bill.YEN_1000);
list.add(Bill.YEN_5000);
list.add(Bill.YEN_5000);
list.add(Bill.YEN_5000);
list.add(Bill.YEN_10000);

int total = 0;
for (final var bill : list) {

    switch (bill) {
        case YEN_1000 -> total += 1000;
        case YEN_5000 -> total += 5000;
        case YEN_10000 -> total += 10000;
    }
}

// 合計 : 27000 円
System.out.println("合計 : " + total + " 円");

for文で条件分岐の処理が必要となりました。
コードとしては、できる限り条件分岐は減らしたいですね。

フィールドとメソッドを追加

それでは、Bill (紙幣) 列挙型を少し拡張してみましょう。
それぞれの列挙型定数に、実際の円の値を整数で持つようにします。

enum Bill {
    YEN_1000(1000),
    YEN_5000(5000),
    YEN_10000(10000),
    ;

    private final int yen;

    Bill(int yen) {
        this.yen = yen;
    }

    int getYen() {
        return yen;
    }
}

円の値を保持するために、フィールドに

private final int yen;

を用意します。
そして、円の初期値を受け取るコンストラクタを用意します。

Bill(int yen) {
    this.yen = yen;
}

列挙型定数の初期化時に、上記のコンストラクタを呼び出すようにします。

YEN_1000(1000),
YEN_5000(5000),
YEN_10000(10000),

そして、円の値を取得するメソッドを用意します。

int getYen() {
    return yen;
}

最後に、列挙型定数の定義部分と、フィールドやコンストラクタといった拡張部分を

 ; (セミコロン)

で区切ります。

YEN_1000(1000),
YEN_5000(5000),
YEN_10000(10000),
;

これは

YEN_1000(1000),
YEN_5000(5000),
YEN_10000(10000);

としても、かまいません。

それでは、拡張した列挙型を使って、紙幣リストの円の合計を計算してみます。

final var list = new ArrayList<Bill>();

list.add(Bill.YEN_1000);
list.add(Bill.YEN_1000);
list.add(Bill.YEN_5000);
list.add(Bill.YEN_5000);
list.add(Bill.YEN_5000);
list.add(Bill.YEN_10000);

int total = 0;
for (final var bill : list) {
    total += bill.getYen();
}

// 合計 : 27000 円
System.out.println("合計 : " + total + " 円");

switchによる条件分岐がなくなり、for文の処理がすっきりしましたね。
Stream APIを使うと、さらにスマートに記述できます。

final var total = list.stream().mapToInt(Bill::getYen).sum();

// 合計 : 27000 円
System.out.println("合計 : " + total + " 円");

抽象メソッドを追加

列挙型に抽象メソッドを宣言して、それぞれの列挙型定数で実装するアプローチも見てみましょう。
計算の結果は同じとなります。

enum Bill {
    YEN_1000 {
        @Override
        int getYen() {
            return 1000;
        }
    },
    YEN_5000 {
        @Override
        int getYen() {
            return 5000;
        }
    },
    YEN_10000 {
        @Override
        int getYen() {
            return 10000;
        }
    },
    ;

    abstract int getYen();
}

シールクラス

列挙型定数は、インスタンスは常に1つという制限があります。

もし、列挙型のような網羅性を維持しつつ、インスタンスを複数生成したい…そんなときはシールクラスが使えるかもしれません。
シールクラスについては下記の記事にまとめていますので、よろしければご参照ください。

まとめ

列挙型、いかがでしたでしょうか。
単純な定数の定義だけではなく、ある程度の拡張も可能です。

有効に活用していきましょう。


関連記事

ページの先頭へ