S-JIS[2008-06-05/2017-02-13] 変更履歴

Java列挙型

列挙型(enum)は、JDK1.5で導入された、一連の値を定義する文法。[2006-07-26]
一番単純な定義方法はC言語の列挙型に似ているが、C言語と違って実態はクラス(と、不変オブジェクト)。

enumはenumerate(イニュームレイトあるいはイヌームレイト)の略なので、発音としては「イニューム」が正しそうだが、自分は「イナム」「エナム」と呼んでいる。


最も単純な定義方法

enum 列挙名 { 列挙子, 列挙子, … }

列挙名は、クラス名(やインターフェース名など)と同様。
(したがって、publicの付いたenumの列挙名は、ソースファイル名と同じでなければならない。また、publicの付いたenumはソースファイル内に1つしか定義できない)

列挙子(enum定数)は、フィールド名の定数(クラス内のstatic final変数)と同様。
(したがって、アルファベット大文字を使うのが通例)


列挙子の最後にセミコロンが付いていても大丈夫。

enum 列挙名 { 列挙子, 列挙子, …; }

また、C言語のenumと同様、最後の列挙子の末尾にカンマが付いていても大丈夫。(ただ単に無視される)
カンマが末尾に付けられないと、ソースをコピー&ペーストしてカンマを消し忘れるとコンパイルエラーになる。ま、ちょっとした利便性の追求結果。

enum 列挙名 { 列挙子, 列挙子, …, }

合わせて、最後にカンマとセミコロンが付いていても大丈夫…ちょっと変だが。

enum 列挙名 { 列挙子, 列挙子, …,; }

例:

package sample.enums;

enum Sex { MAN, WOMAN }
enum Status { STAT1, STAT2, STAT3; }
enum 人族 {
	人間,
	エルフ,
	ドワーフ,
	タビット,
	ルーンフォーク,
	ナイトメア,	←最後がカンマで終わっていてもOK。なので追加削除や順序変更(コピペ)がしやすい。
}

使用方法

定義した列挙子は、定数(public static finalなフィールド)のように扱う。

import sample.enums.Status;
	Status s = Status.STAT1;

	if (s == Status.STAT1) {
		〜
	} else if (s == Status.STAT2) {
		〜
	}
import sample.enums.Status;
import static sample.enums.Status.*;
	Status s = STAT1;

	if (s == STAT1) {
		〜
	} else if (s == STAT2) {
		〜
	}
  staticインポートを使えば、
接頭辞としてのクラス(enum)指定を
省略することが出来る。

equals()で比較しても良いが、列挙の場合はオブジェクト(列挙子)が増えないことが保証されている(新しくnewで作られることは無い)ので、==演算子でも大丈夫。

でもプリミティブ型じゃないんだから、equals()を使う方がいいんだろうか。
でもequals()の中も==演算子で比較してるしなぁ。しかもfinalメソッドだから変更されることも無いし。


また、enumはswitch文で使うことも出来る。

import sample.enums.Status;
	Status s = Status.STAT1;
	switch (s) {
	case STAT1:	←caseに指定する場合は、クラスの指定「Status.」は不要(というか、付けるとコンパイルエラー)
		〜
		break;
	case STAT2:
		〜
		break;
	default:
		〜
		break;
	}

なお、switch()に渡す値がnullだとNullPointerExceptionが発生するので注意。

switchの実装(展開のされ方)


暗黙に定義されるメソッド

列挙(enum)は、コンパイルされた実態は、クラス(class)と不変オブジェクトになる。いわゆるTypesafe Enumというものらしい。
(不変と言っても基本部分だけ。列挙用のクラスはプログラマーが拡張できるので、その部分はプログラマー次第)

enum Status { STAT1, STAT2, STAT3 }

↓(コンパイルされたイメージ。実際はちょっと違う)

final class Status extends Enum<Status> {
	private Status(String name, int ordinal) {
		super(name, ordinal);
	}

	public static final Status STAT1 = new Status("STAT1", 0);
	public static final Status STAT2 = new Status("STAT2", 1);
	public static final Status STAT3 = new Status("STAT3", 2);

	private static final Status ENUM$VALUES[] = { STAT1, STAT2, STAT3 };

〜
}

変換されたクラスは、JDK1.5で導入されたEnumというクラスを継承している。

また、列挙子自体は、static finalな定数(不変オブジェクト)として定義されている。
コンストラクターの第1引数は列挙子の名前そのもので、第2引数は定義した順序に従った番号(序数)。
ちなみに、列挙子のオブジェクトは、リフレクションフィールドとして取得して属性を見てみるとisEnumConstant()がtrueになっている。

ENUM$VALUESは列挙子全てを格納した配列だが、あくまでも暗黙の変数(合成フィールド)なので、プログラムから使うことは出来ない。values()で使用されている。

Enumを継承しているので そこで定義されているメソッドが使える他、staticメソッドも定義される。(staticメソッドなので、Enumからの継承ではなく、コンパイラーが生成する)

列挙型・列挙子で使えるメソッド
メソッド 説明 使用例
public final String name() 列挙子の名前を返す。コンストラクターで与えられたname。 String name = STAT1.name(); STAT1
public final int ordinal() 列挙子の序数(順序番号)を返す。コンストラクターで与えられたordinal。 int n = STAT1.ordinal(); 0
public final int compareTo(列挙子) 序数に従って大小比較を行う。 Status s = 〜;
if (s.compareTo(STAT2) < 0) 〜
 
public String toString() name()と同じものを返す。オーバーライド可 System.out.println(STAT1); STAT1
public static 列挙名 valueOf(String name) 文字列(列挙子の名前)に該当する列挙子のオブジェクトを返す。
見つからない場合はIllegalArgumentExceptionが発生する。
Status s = Status.valueOf("STAT1");  
public static 列挙名[] values() 列挙子を順番に並べた配列を返す。
これはENUM$VALUESコピーなので、何度も呼び出すのはちょっと効率が悪い。
Class#getEnumConstants()
Status[] sa = Status.values();  
public final Class<列挙名> getDeclaringClass() 列挙子のクラスを取得する。
大抵はgetClass()と同じだが、getDeclaringClass()の方が正しいクラスを返す。
Class clazz = STAT1.getDeclaringClass();  

valueOf()values()はコンパイラーが生成するメソッドだが、合成メソッド扱いではない。したがってプログラムから使用することが出来る。


独自メソッド定義

C言語の列挙型は実態は整数型だったので、初期値を与えることが出来た。(初期値を与えない場合、0からの連番)

C言語の列挙で初期値を与える例:

enum {
	STAT1 = 100,	/* STAT1の値は100になる */
	STAT2,		/* 前の列挙子の値に+1したもの、つまり101になる */
	STAT3		/* 102になる */
};

Javaの列挙型は上述の通りクラスなので、C言語のような初期値を与えることは出来ない。

その代わり、メソッドやフィールドを定義することが出来る。
(親であるEnumのメソッドをオーバーライドすることも可能…と言ってもname()ordinal()finalメソッド(つまりオーバーライド不可)なので、実質的にはtoString()くらいしかオーバーライドできない)
(親クラスが決まっているので別のクラスを継承することは出来ないが、インターフェースを実装することは出来る。→fumokmmさんのJavaのenumは継承できないけどインタフェースが継承できる [2012-05-03]
コンストラクターを定義することも出来るので、そこで独自の初期値を渡してやればよい。

メソッドやフィールドを定義する例:

enum Status2 {
	//列挙子は必ずenum内の先頭に書かなければならない。
	STAT1, STAT2, STAT3;
	//また、この後ろにフィールドやメソッドを書くので、列挙子の最後にセミコロンが必須。

	@Override
	public String toString() {
		return "<" + name() + ">";
	}
}
enum ArgStatus {
	//列挙子の定義は、コンストラクターに合わせた引数を持たせる。
	STAT1(100), STAT2(101), STAT3(102);

	private int value;

	//enum内に定義できるコンストラクターは、privateのみ可能。他はコンパイルエラーになる。
	private ArgStatus(int n) {
		//super("zzz", n);		enumのコンストラクターは、super()を明示的に呼ぶことが出来ない。
		this.value = n;
	}

	public int getValue() {
		return this.value;
	}
}

		System.out.println(ArgStatus.STAT1.getValue()); → 100が返る

コンストラクターを複数定義した場合は、列挙子の定義もそれらに合わせた引数を使い分ければよい。

↓コンストラクターを定義した場合、実態のクラスは以下のようになる。

final class ArgStatus extends Enum<ArgStatus> {
	private int value;

	private ArgStatus(String name, int ordinal, int n) {
		super(name, ordinal);
		this.value = n;
	}

	public int getValue() {
		return this.value;
	}

	public static final ArgStatus STAT1 = new ArgStatus("STAT1", 0, 100);
	public static final ArgStatus STAT2 = new ArgStatus("STAT2", 1, 101);
	public static final ArgStatus STAT3 = new ArgStatus("STAT3", 2, 102);
〜
}

つまり、独自にコンストラクターを定義した場合でも そのコンストラクターに暗黙の引数(nameとordinal)が追加されるので、name()ordinal()基本通り使用することが出来る。


個々の列挙子にも独自のメソッドを実装(オーバーライド)することが出来る。
※「列挙クラスで作ったメソッドに対してオーバーライドすることが出来る」という意味であって、「個々の定数毎に(オーバーライドしない)独自メソッドが作れる」という意味ではない。

enum Status3 {
	STAT1,
	STAT2,
	STAT3 {
		@Override
		public String getString() {
			return "ここだけ独自実装";
		}
	},
	STAT4;

	public String getString() {
		return name();
	}
}
		Status3[] arr = Status3.values();
		for (Status3 s : arr) {
			System.out.println(s.getString());
		}

※オーバーライドしたメソッド内から列挙クラス内に用意したフィールドにアクセスしたい場合、そのフィールドはprivateではダメ(protectedならOK)[2009-09-23]

実行結果:

STAT1
STAT2
ここだけ独自実装
STAT4

↓逆コンパイルして見ると、列挙子に書いたメソッド定義は無名内部クラスを上手く利用している。

final class Status3 extends Enum<Status3> {
〜
	public String getString() {
		return name();
	}

	public static final Status3 STAT1 = new Status3("STAT1", 0);
	public static final Status3 STAT2 = new Status3("STAT2", 1);
	public static final Status3 STAT3 = new Status3("STAT3", 2) {
		@Override
		public String getString() {
			return "ここだけ独自実装";
		}
	};
	public static final Status3 STAT4 = new Status3("STAT4", 3);
〜
}

なお、本来はfinalクラス(この例ではStatus3)を継承した新しいクラス(この例ではSTAT3を表す無名内部クラス)を定義することは出来ないのだが(無名内部クラスも同様)、列挙子については特別に出来るようだ。

この場合の列挙子のクラスを取得してみると、ちょっと面白い。

		Status3[] arr = Status3.values();
		for (Status3 s : arr) {
			System.out.println(s);
			System.out.println(s.getClass());
			System.out.println(s.getDeclaringClass());
		}
STAT1
class jp.hishidama.sample.enums.Status3
class jp.hishidama.sample.enums.Status3
STAT2
class jp.hishidama.sample.enums.Status3
class jp.hishidama.sample.enums.Status3
STAT3
class jp.hishidama.sample.enums.Status3$1
class jp.hishidama.sample.enums.Status3
STAT4
class jp.hishidama.sample.enums.Status3
class jp.hishidama.sample.enums.Status3

STAT3は無名クラスなので、他の列挙子のクラスと違って、自動的に作られたクラス名になっている。
すなわち、getClass()を使って取得したクラスからは、自分の属している列挙型かどうかは(直接は)判断できない。
getDeclaringClass()は自分が属している(自分を定義している)列挙型を正しく返す。


独自の静的初期化子

enumでも静的初期化子(static initializer)を定義することが出来る。[2017-02-13]
この中でvaluesメソッドを呼び出しても大丈夫。

public enum Status {
	STAT1, STAT2, STAT3;

	private static final Map<String, Status> MAP = new HashMap<>();
	static {
		for (Status s : values()) {
			MAP.put(s.name().toLowerCase(), s);
		}
	}
}

なお、enumのコンストラクター内でstaticフィールドにアクセスすることは出来ない。
以下のようなコーディングだとコンパイルエラーになる。

public enum Status {
	STAT1, STAT2, STAT3;

	private static final Map<String, Status> MAP = new HashMap<>();

	Status() {
		MAP.put(name().toLowerCase(), this);	// Cannot refer to the static enum field Status.MAP within an initializer(初期化子からstaticフィールドへの参照が不正です)
	}
}

各列挙子はenumの中でstaticフィールドとして保持される。
また、valuesメソッドで使用する列挙子一覧(配列)をstaticフィールドで保持している。
そこに独自のstaticフィールドおよび静的初期化子が加わると、初期化順序は、
列挙子のインスタンス生成→values用の列挙子一覧の生成→staticフィールドの初期化および静的初期化子の実行
という順序になる。
したがって、列挙子のコンストラクターが呼ばれるタイミングではstaticフィールドはまだ初期化されていない為、使用することは出来ない。
(上記の例で、もしコンパイルエラーにならず実行できたとしても、コンストラクターの中で(MAPがまだ初期化されていないので)MAPがnullだからNullPointerExceptionになるだろう)


フラグとしての論理和

C言語の列挙型は整数なので、ビット単位で別の値になるように数値を割り当て、論理和を使って複数のフラグが立っているかどうかを保持するような使い方が出来た。

C言語の例:

enum {
	FLAG0 = 1 << 0,	/* 0x01 */
	FLAG1 = 1 << 1,	/* 0x02 */
	FLAG2 = 1 << 2,	/* 0x04 */
	FLAG3 = 1 << 3,	/* 0x08 */
};

		int flag = FLAG0 | FLAG1 | FLAG3;
		if (flag & FLAG0) {
			printf("フラグ0が立っている\n");
		}

Javaの列挙型は整数ではないので、直接このような使い方は出来ない。
しかしこういう目的の為にEnumSetというクラスが用意されている。

EnumSetの例:

enum Flag { FLAG0, FLAG1, FLAG2, FLAG3 }

		EnumSet<Flag> flag = EnumSet.of(FLAG0, FLAG1, FLAG3);
		if (flag.contains(FLAG0)) {
			System.out.printf("フラグ0が立っている%n");
		}

EnumSetはHashSet等と異なり、newでインスタンス化することは出来ない。
インスタンスを生成するstaticメソッドを呼び出すことによって準備する。

メソッド 説明 使用例
EnumSet<列挙名> noneOf(列挙名.class) 空のセットを作成する。 EnumSet<Flag> none = EnumSet.noneOf(Flag.class);
EnumSet<列挙名> allOf(列挙名.class) 全ての要素を含んだセットを作成する。 EnumSet<Flag> all = EnumSet.allOf(Flag.class);
EnumSet<列挙名> of(列挙子, …) 指定された要素を含んだセットを作成する。 EnumSet<Flag> flag = EnumSet.of(FLAG0);
EnumSet<列挙名> range(列挙子, 列挙子) 指定された範囲のセットを作成する。 EnumSet<Flag> rng = EnumSet.range(FLAG0, FLAG2);
boolean add(列挙子) 指定された要素を追加する。(フラグを立てる) flag.add(FLAG3);
boolean remove(列挙子) 指定された要素を削除する。(フラグをクリア) flag.remove(FLAG0);
boolean contains(列挙子) 指定された要素を含んでいるかどうか。
(フラグが立っているかどうか)
if (flag.contains(FLAG1)) 〜

キーを列挙型にしたMapを使いたい場合は、(HashMapでなく)EnumMapを使うと実行効率が良い。
(列挙子の個数が固定なので、序数を使った配列で実装されている)

	Map<Status, String> map = new EnumMap<Status, String>(Status.class);
	map.put(STAT1, "abc");

switchの実装

列挙型を使ったswitch文のコンパイル結果を逆コンパイルしてみると、意外な実装になっている。

	public void method(Status s) {
		switch (s) {
		case STAT1:
			System.out.println("1です"); break;
		case STAT2:
			System.out.println("2です"); break;
		default:
			System.out.println("その他"); break;
		}
	}

コンパイラーの種類(SunEclipse)で多少違うが、少なくともswitch()用に序数を取得する為にordinal()をいきなり呼び出しているので、もし変数 (switchに渡している値)がnullだったらNullPointerExceptionが発生するので注意。[/2008-07-07]

↓(Sunのjavac(JDK1.5・1.6)の場合)[/2008-07-07]

	public void method(Status s) {

		static class _cls1 { //局所クラス
			static final int[] $SwitchMap$sample$enums$Status = new int[Status.values().length];

			static {
				try {
					$SwitchMap$sample$enums$Status[Status.STAT1.ordinal()] = 1;
				} catch(NoSuchFieldError e) {
					// 無処理
				}
				try {
					$SwitchMap$sample$enums$Status[Status.STAT2.ordinal()] = 2;
				} catch(NoSuchFieldError e) {
					// 無処理
				}
				try {
					$SwitchMap$sample$enums$Status[Status.STAT3.ordinal()] = 3;
				} catch(NoSuchFieldError e) {
					// 無処理
				}
			}
		}

		switch (_cls1.$SwitchMap$sample$enums$Status[s.ordinal()]) {
		case 1:
			System.out.println("1です"); break;
		case 2:
			System.out.println("2です"); break;
		default:
			System.out.println("その他"); break;
		}
	}

↓(Eclipse3.2(JDK1.5・1.6)の場合)[/2008-07-07]

	public void method(Status s) {
		switch ($SWITCH_TABLE$sample$enums$Status()[s.ordinal()]) {
		case 1:
			System.out.println("1です"); break;
		case 2:
			System.out.println("2です"); break;
		default:
			System.out.println("その他"); break;
		}
	}

	private static int[] $SWITCH_TABLE$sample$enums$Status;

	static int[] $SWITCH_TABLE$sample$enums$Status() {
		if ($SWITCH_TABLE$sample$enums$Status == null) {

			int[] ai = new int[Status.values().length];
			try {
				ai[Status.STAT1.ordinal()] = 1;
			} catch (NoSuchFieldError e) {
				// 無処理
			}
			try {
				ai[Status.STAT2.ordinal()] = 2;
			} catch (NoSuchFieldError e) {
				// 無処理
			}
			try {
				ai[Status.STAT3.ordinal()] = 3;
			} catch (NoSuchFieldError e) {
				// 無処理
			}
			$SWITCH_TABLE$sample$enums$Status = ai;
		}
		return $SWITCH_TABLE$sample$enums$Status;
	}

$SWITCH_TABLEから始まるメソッドを呼び出し(その中で同名の配列を初期化し)、intの配列を取得している。
この配列が序数(ordinal)毎のswitch〜case用のインデックスを保持している。


それにしても、直接ordinal()を使えば良さそうなものだが…

	public void method(Status s) {
		switch(s.ordinal()) {
		case 0:
			System.out.println("1です"); break;
		case 1:
			System.out.println("2です"); break;
		default:
			System.out.println("その他"); break;
		}
	}

実のところ、列挙型の列挙子に増減が無いなら(もしくは増減した際に使ってる側を全てリコンパイルするなら)、ordinal()を直接使うのが一番早いと思う。
しかしもし列挙子が増えたり減ったりするとordinal()の値は変わるので、(switch側をリコンパイルしないなら)困った事になる。

enum Status { STAT1, STAT2, STAT3 }

enum Status { STAT1, STAT_ADD, STAT2, STAT3 }

という追加をすると、STAT2の序数は1から2に変わる。
しかしcaseに序数を直接使っていたら、caseの部分をコンパイルし直さない限り、caseは以前の値のままなのでずれてしまう!

列挙型を変更する度にその列挙型を使っている全ソースをコンパイルするならそれでも構わないだろうけど…、必ずコンパイルするとは限らないわな。
他のクラス内の(こちらが使っていない)フィールドがいくら変わろうが無関係(リコンパイルしない)なはずだし。(列挙型の実際の姿はフィールドなので)

ちなみに、整数を使ったswitch〜caseにするのではなく、if文を使ってひとつひとつの列挙子と比較する方式であれば、列挙子が増えた場合には対応できる。

	public void method(Status s) {
		if (s == STAT1) {
			System.out.println("1です");
		} else if (s == STAT2) {
			System.out.println("2です");
		} else {
			System.out.println("その他");
		}
	}

…あれ? breakを使わないで文が続くときはどうすればいいんだろう?(爆)


では$SWITCH_TABLE$SwitchMap)を使う方式ではこういう場合どうなるかと言うと、

列挙子が増えた場合
case用の配列は実行時に作っているので、必ず新しい序数が使われる。
このため、caseに使っていた値(インデックス値)とは必ず合致する。(配列内の列挙子の位置が変わるだけなので)
増えた列挙子は(コンパイルし直さない限り)switch文で使われているはずが無いので、その値が配列に入らなくても問題は無い。
その場合、その列挙子の序数の配列内の値は(設定されないので)0になる。caseの連番は1から始まっているので、必ずdefaultに行くことになる。これは元のソースに書かれている動作と合致している。
 
列挙子が減った場合
実行時に列挙子(フィールド)が存在しないので、NoSuchFieldErrorが発生する。
でも見て分かるとおり、その例外(実行時エラー)は握りつぶしているので、問題は無い。

一見とんでもない実装に見えるけど、ちゃんと理に適っている。

でもこの$SWITCH_TABLEのメソッドはswitch文を使っているクラス毎にそれぞれ作られるわけだから、その分はけっこう無駄な気もするなぁ。
(しかも異なるenumのswitch文を使えば、個別に$SWITCH_TABLEのメソッドが作られるわけだし)
(classファイルのサイズだけの問題かもしれないけど)

例外を1個1個握りつぶすのも負荷が高そうだなぁ。一番最初に初めて呼ばれたときだけとは言え、その分初回実行は遅くなるわけだし。
とは言っても現実問題としては、列挙子が増えることはあっても減ることは滅多に無い気がするので、あまり気に病むことも無いか。
(コンパイルし直せば解消するんだし)

Sunのjavacでは、メソッド内に局所クラスを作り、その中の静的初期化子を使ってクラスの初期化時に配列も初期化している。[/2008-07-07]
Eclipse3.2では初期化部分がメソッド化されていて、配列が初期化されていない場合だけ初期化している。
つまり、もしswitch文を含むメソッドが実行されない場合、Sunの方式では初期化の実行は無駄になる。Eclipse方式ではメソッド化することによってクラスロード時の起動(初期化)を高速化しているのだろう。
ちなみにEclipse方式の初期化部分で配列に直接代入せずにローカル変数を使っているのは、マルチスレッドで呼ばれても大丈夫なようにする為だと思われる。


参考:


enumの言語仕様上の特殊扱い

enumはclassの特殊形態であり、Java言語仕様上で特別扱いされている。[2008-09-14]

ポイント 概要 説明
new enumはnewで新しいインスタンスを作ることが出来ない。 コンストラクターがprivateになっている。
しかも、newしようとした際はストレートに「列挙型はインスタンス化できない」というコンパイルエラーになる。
通常のクラスでは、privateなコンストラクターを使ってnewしようとすると「コンストラクターが不可視」というコンパイルエラーになる。
newInstance() リフレクションによるインスタンス生成も行えない。 列挙クラスに対してnewInstance()を呼び出すと、InstantiationExceptionが発生する。
通常のクラスでは、privateなコンストラクターであればIllegalAccessExceptionが発生する。
clone() enumはクローンを作れない。 列挙は必ずEnumクラスを継承しているが、Enumでclone()をfinalメソッドとしてオーバーライドしており、
呼ばれると 常にCloneNotSupportedExceptionを発生させる。
シリアライズ 外部に出力して再度読み込んだ場合に、正しい列挙子になる。 列挙子に増減があってordinalの値が変わったとしても、正しい(名称の)列挙子になる。
また、列挙子が無くなった場合はInvalidObjectException(enum constant 列挙子 does not exist)が発生する。
抽象メソッド enumで抽象メソッドを定義したら、各列挙子でそのメソッドを実装しなければならない。[2017-02-13] 抽象メソッドのままだとインスタンス化できないので、当然^^;
ファイナライザー enumはファイナライザーを定義できない。[2017-02-13] finalizeメソッドは、親クラスであるEnumでfinalメソッドになっているので、オーバーライドできない。

ちなみに、enumはあくまで「特殊なclass」なので、以下のようなコーディングも出来はする。

public enum Main {
	;

	public static void main(String[] args) {
		System.out.println("ちゃんとjavaコマンドから実行できるよーん");
	}
}

Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま