Javaについて徹底解説!

Javaの定数はEnumで! Enumの使い方から考え方までお伝えします

大石 英人

開発エンジニア/Java20年/Java GOLD/リーダー/ボールド歴2年

JavaでのEnumは「列挙型」と呼ばれるものですと言われても良く分かりませんよね。列挙と言う言葉自体を、日常生活ではあまり使いません。Enumは、Javaで定数として扱えるものだと思っていただければ、まずはOKです。

もちろん、Enumは単なる定数だけの機能では終わりません。Enumは実はクラスの一種ですから、もっとすごいことが出来ますし、上手く使えばプログラムの品質アップに効果絶大です!!

この記事ではそんなEnumについて初心者向けに説明していきます。Enumの基本から活用方法まで幅広くカバーします。

※この記事のサンプルは、Java 10の環境で動作確認しています。


1.Enumの基本

Enumはenumerationの略で、「列挙、数えあげること、目録、一覧表」(Weblio英和辞典)という意味があります。分かりやすいのは一覧表でしょうか。ですので、JavaEnumも何かの一覧だと考えると分かりやすいでしょう。

ここでは、そんなEnumの基本についてお伝えします。

1-1.Enumの宣言の仕方

Enumは以下の構文で宣言します。Enumを宣言したファイル名は「Enum.java」となります。クラスと同じルールですね。

enum Enum名 {
	Enumとしたい値の名前を","区切りで列挙する
}

以下がEnumの簡単な例です。ファイル名はSeason.javaになります。この例では、季節をEnumにしてみました。つまり、このEnumでは「季節(Season)は、春(SPRING)・夏(SUMMER)・秋(AUTUMN)・冬(WINTER)のいずれかである」と宣言しています。

enum Season {
	SPRING,
	SUMMER,
	AUTUMN,
	WINTER
}

パッと見で、クラスとフィールドの宣言に似ているのが分かるでしょうか。“class”の代わりに“enum”としていますし、SPRING等を記述している箇所も普通のクラスならフィールドの位置です。

Enumと同等のことをクラスで行う場合は、以下が比較的近いものです。ただし、これはEnumと全く同じではなく、Enum独自の便利な機能もたくさんあります。それが通常のクラスとEnumの違いになります。

class Season {
	public static final Season SPRING = new Season();
	public static final Season SUMMER = new Season();
	public static final Season AUTUMN = new Season();
	public static final Season WINTER = new Season();

	private Season() {
	}
}

ちなみに、Enumの値の名称を大文字にしているのは、単にJavaでは定数は英語大文字とするのが慣習だからです。ですので、別に小文字でもコンパイルエラーにはなりません。でも、Javaの標準APIでもEnumの値は全て「大文字かつ単語は“_”つなぎ」なので、このルールを守っておくのが良いでしょう。

1-2.Enumはクラスと同じように扱える

Enumの使い方は、クラスにおけるstaticフィールドとほぼ同じです。Enumの値すなわちインスタンスへのアクセスは、プログラム上からはstaticフィールドへのアクセスと同じ「Enum.値の名称」です。

では、先程作ったEnumSeasonを使ってみましょう。

System.out.println(Season.SPRING); // → "SPRING"が出力される

当然ながら、Seasonであらかじめ決められたもの以外は使えません。

System.out.println(Season.ANYTHING); // → コンパイルエラー!! ("ANYTHING は解決できないか、フィールドではありません")

Enumで宣言した値は、Enum自身のインスタンスです。StringIntegerなどではありません。ですので、この例では「季節の一つとしての春夏秋冬」がJava上でもそのまま表現できているのです。

if (Season.SPRING instanceof Season) {  // → trueになる
	System.out.println("Season.SPRING は Season のインスタンスです");
}

Enumはプリミティブ型やクラスと同じように型として扱えますので、変数として宣言したり、Enumのインスタンスを参照させられます。

Season season1 = Season.SPRING;
Season season2 = Season.SUMMER;

もちろん、メソッドへの引数や戻り値ともできます。

class EnumExample {
	void printSeason(Season season) {
		System.out.println(season);
	}

	void callPrintSeason() {
		printSeason(Season.SPRING);
		printSeason(Season.SUMMER);
		printSeason(Season.AUTUMN);
		printSeason(Season.WINTER);
	}

	Season getSeason() {
		return Season.SPRING;
	}
}

このように、Enumは普通のクラスのインスタンスと同じように扱えるものなのです。

1-3.【重要】Enumは新しいインスタンスを生成できない

Enumが普通のクラスと違う最大のポイントは「新しいインスタンスの生成(new)Enumの外部からはできない」ことです。つまり、Enumとして存在し得るインスタンスは、あらかじめ宣言したものだけなのです。

これがEnumの特徴および存在意義で、だからこそEnumの各インスタンスはプログラムの中で1つしかないことが保証される「定数」として扱えるのです。

Season season = new Season(); // コンパイルエラー!! ("型 Season のインスタンスを生成できません")

2.Enumの使用例

2-1.Enum同士の比較(if文・switch)

最も頻繁に使うのは、Enum型の値同士を比較して、どのEnumの値かを調べることです。

例えば、if文・switch文での使用例は以下のとおりです。直感的に「きっとこうなるだろう」というとおりの動きですよね?

Season season = Season.SPRING;

if (season == Season.SPRING) {
	System.out.println("春!!"); // → ここに該当する
} else if (season == Season.SUMMER) {
	System.out.println("夏!!");
} else if (season == Season.AUTUMN) {
	System.out.println("秋!!");
} else if (season == Season.WINTER) {
	System.out.println("冬!!");
}
Season season = Season.SUMMER;

switch (season) {
case SPRING:
	System.out.println("春!!");
	break;
case SUMMER:
	System.out.println("夏!!"); // → ここに該当する
	break;
case AUTUMN:
	System.out.println("秋!!");
	break;
case WINTER:
	System.out.println("冬!!");
	break;
}

2-1-1.【重要】Enum同士の比較は==で確実に行える

Enum同士の比較は、上記例のとおり == で行うのがお手軽かつ確実です。equalsでもいいのですが、タイプ量が多くなりますし、プログラムの見た目がごちゃごちゃしてしまいます。

Javaのequalsについてしっかり理解されている方は、Stringなどの事例から、「==だと、保持する値は同じだけれどインスタンスが違うならfalseになってしまうのでは?」と心配される向きもあるでしょう。

でもEnumに関してなら心配無用です。Enumの値(=インスタンス)Java上で必ず1つだけになることが保証されていますので、Enumのインスタンスの比較は==で良いのです。

2-2.Enumが持つメソッドの使い方

Enumは標準で幾つかのメソッドを持っています。以下で説明するものは、Enumであれば宣言・実装をしないでも共通的に使えるものです。便利に使いましょう。

2-2-1.nameはEnumの名称を戻す

nameはEnumの値の名称をStringとして戻します。変数が参照しているのがどのEnumの値かを表示・確認したり、ログなどに出力するのに使います。

Season season = Season.SUMMER;
System.out.println(Season.SPRING.name()); // → "SPRING"
System.out.println(season.name()); // → "SUMMER"、変数seasonが指しているのはSUMMERであるため

2-2-2.ordinalはEnumの宣言された順番を戻す

ordinalは、Enum上で値が宣言された順番を数値で戻します。順番は0始まりです。

Season season = Season.SUMMER;
System.out.println(Season.SPRING.ordinal()); // → 0
System.out.println(Season.SUMMER.ordinal()); // → 1
System.out.println(Season.AUTUMN.ordinal()); // → 2
System.out.println(Season.WINTER.ordinal()); // → 3
System.out.println(season.ordinal()); // → 1、変数seasonが指しているのはSUMMERであるため

2-2-3.valueOfは名称の文字列のEnumを戻す

valueOfは名称の文字列に対応するEnumを戻します。valueOfEnumstaticメソッドなので、値に対しては使うものではありません。言葉だと少しわかりづらいかもしれませんが、例を見れば一目瞭然です。

String name = "AUTUMN";
Season season = Season.valueOf(name);
System.out.println(Season.valueOf("SPRING")); // → SPRING
System.out.println(Season.valueOf("SUMMER")); // → SUMMER
System.out.println(Season.valueOf("AUTUMN")); // → AUTUMN
System.out.println(Season.valueOf("WINTER")); // → WINTER
System.out.println(season); // → AUTUMN、変数seasonが指しているのはAUTUMNであるため

これは、例えばメソッドにStringとして渡された名称からEnumの値を取得するのに使えます。

ちなみに、宣言されたもの以外の名称を渡すと例外が発生します。

Season season = Season.valueOf("NOT_DEFINED_IN_ENUM"); // → IllegalArgumentExceptionが発生する

2-2-4.valuesは全てのEnumの配列を返す

valuesは全てのEnumの配列を返すstaticメソッドです。全ての値に対して何か処理をしたい場合に使います。

Season[] seasons = Season.values();

for (Season season : seasons) {
	System.out.println(season); // → SPRING、SUMMER、AUTUMN、WINTERが順に表示される
}

3.【応用】Enumの進んだ使い方

3-1.Enumのインスタンスに固有の値を持たせる

Enumのインスタンスに固有の値を持たせたい場合があります。例えば、処理結果の種類を表すEnumであれば、それを数値で表したコードを持たせるなどです。

その場合は、Enumのインスタンスフィールドに、コンストラクタで値を設定するのがお勧めです。実はEnumもクラスの一種なので、コンストラクタ、フィールド、メソッドなどを定義できるのです。

enum ResultCode {
	NORMAL(100),
	WARNING(200),
	ABNORMAL(999);

	private final int code;

	private ResultCode(int code) {
		this.code = code;
	}

	int getCode() {
		return code;
	}
}

Enumのフィールドは、そのフィールドのコンストラクタを呼び出してインスタンスを生成しているのだと解釈してください。ですから、引数を持つ独自のコンストラクタを定義するなら、引数を与えるのはフィールド定義のところになるのです。

ここでのポイントは、フィールドをprivate finalとしてコンストラクタ内での設定を強制していることと、設定値をコンストラクタの引数としていること、フィールドの値にアクセスすためのgetterメソッドを作っていることです。

こうすることで、このEnumは以下のように使えます。その他のEnumの標準機能(nameordinal)は、普通のEnumと同様に使えます。

System.out.println(ResultCode.NORMAL.getCode()); // → 100
System.out.println(ResultCode.WARNING.getCode()); // → 200
System.out.println(ResultCode.ABNORMAL.getCode()); // → 999

3-2.インスタンスごとに固有のメソッド実装を持たせる

Enumには前述のとおりメソッドを実装できます。さらに、それらのメソッドをEnumのインスタンスごとにオーバーライドさせられます。このように、オブジェクトとしての振る舞いを自由にカスタマイズできるのが、EnumStringintによる定数以上の強力な機能を持っているということの証明です。

やり方は二種類あり、標準動作をするメソッドを宣言して個別のインスタンスでオーバーライドする方法、抽象メソッドを宣言して個別のインスタンスで実装する方法です。以下にその二種類の例を示します。

// 標準のメソッドを宣言し、特定の値でだけオーバーライドする場合
enum ResultCode {
	NORMAL {
		int doSomething() {
			return 12345;
		}
	},
	WARNING,
	ABNORMAL;

	int doSomething() {
		return 99999;
	}
}
System.out.println(ResultCode.NORMAL.doSomething()); // → 67890
System.out.println(ResultCode.WARNING.doSomething()); // → 12345
System.out.println(ResultCode.ABNORMAL.doSomething()); // → 12345
// Enumにabstractメソッドを宣言し、全ての値で実装させる場合
enum ResultCode {
	NORMAL {
		int doSomething() {
			return 12345;
		}
	},
	WARNING {
		int doSomething() {
			return 67890;
		}
	},
	ABNORMAL {
		int doSomething() {
			return 99999;
		}
	};

	abstract int doSomething();
}
System.out.println(ResultCode.NORMAL.doSomething()); // → 12345
System.out.println(ResultCode.WARNING.doSomething()); // → 67890
System.out.println(ResultCode.ABNORMAL.doSomething()); // → 99999

3-3.Enumにインターフェイスを実装する

Enumは継承(extends)はできません。ですが、何かのインターフェイスを実装(implements)はできるのです。

例えば、以下のインターフェイスがあるとして、それをEnumに実装してみます。やり方としては上述したメソッド実装・オーバーライドの方法が使えます。必要に応じてやり方を選んでいきましょう。

interface DoSomething {
	int doSomething();
}
// ①ResultCode自身で実装する場合
enum ResultCode implements DoSomething {
	NORMAL,
	WARNING,
	ABNORMAL;

	public int doSomething() {
		return 99999;
	}
}
// ②ResultCodeで実装し、一部のインスタンスではオーバーライドする場合
enum ResultCode implements DoSomething {
	NORMAL {
		public int doSomething() {
			return 12345;
		}
	},
	WARNING,
	ABNORMAL;

	public int doSomething() {
		return 99999;
	}
}
// ③ResultCodeの各インスタンスで固有の実装を持たせる場合
enum ResultCode implements DoSomething {
	NORMAL {
		public int doSomething() {
			return 12345;
		}
	},
	WARNING {
		public int doSomething() {
			return 67890;
		}
	},
	ABNORMAL {
		public int doSomething() {
			return 99999;
		}
	};
}

当然、この場合はEnumの各インスタンスは、Enumでもあり何かのインターフェイスの実装クラスでもあるので、以下のようなプログラミングができるのです。

System.out.println(ResultCode.NORMAL.doSomething()); // インターフェイスのメソッドを呼び出せる

DoSomething doSomething = ResultCode.WARNING; // 型はインターフェイスとしても扱える

if (doSomething instanceof DoSomething) { // 当然instanceofもインターフェイスとして扱える
	System.out.println(doSomething.doSomething());
}

3-4.Enum専用のEnumSet/EnumMapを活用する

EnumはSetの要素やMapのキーとしても使えます。HashSetHashMapでも実用上問題はありませんが、Enum専用のEnumSetEnumMapが存在しますので活用しても良いでしょう。

それぞれの専用Set/Mapを使うメリットは、処理の高速性です。Enumは取り得る値の数が固定であることを前提にチューニングがされています。

EnumSetはコンストラクタとnewを用いてインスタンス生成するのではなく、ofを使ってインスタンスを取得する形になります。一旦インスタンスを取得してしまえば、普通のSetとして使えます。

// Seasonのみ保持できるSetを取得する
Set<Season> enumSet = EnumSet.of(Season.SPRING, Season.WINTER);

System.out.println(enumSet.contains(Season.SPRING));
enumSet.forEach(System.out::println);

EnumMapはnewしてインスタンスを生成します。これもコンストラクタは独特ですが、使い方は普通のMapと同じです。

// キーがSeason、値がStringのEnumMapを生成する
Map<Season, String> enumMap = new EnumMap<Season, String>(Season.class);
enumMap.put(Season.SPRING, "春");
enumMap.put(Season.SUMMER, "夏");
enumMap.put(Season.WINTER, "冬");

System.out.println(enumMap.containsKey(Season.SPRING)); // → true
System.out.println(enumMap.containsKey(Season.AUTUMN)); // → false

for (Entry<Season, String> e : enumMap.entrySet()) {
	System.out.println(e.getKey() + " is " + e.getValue());
}

3-5.Enumで管理するものは注意深く考えよう

Enumは値をあらかじめ決められるのが大きなメリットですが、一つのEnumに数十~数百以上も値があると、さすがに使い勝手やメンテナンス性が悪くなります。

そのような場合は、複数の異なる意味に分割できるようなものも一つのEnumに無理に押し込めようとしている可能性がありますので、意味的な観点から見直しを行いましょう。

Enumはいわゆる定数クラスや定数インターフェイスのように、一つのものにプログラム上で定数として使うであろう全ての種類の値を押し込むのに使うものではありません。この記事の例のとおり、季節や処理結果など、意味のあるものごとにEnumを定義し、それぞれに適切な値を定義して使うものです。

また、詳細なエラーメッセージやエラーコードなどをEnumで全て表現するのも、値の数が多くなる原因です。その場合はどこまでが具体的なEnumとして表現すべきもので、どこからがマニュアルやJavadocで表現すべきものか考えましょう。例えばエラーの分類はEnumとし、詳細コードはStringintとするなどです。


4.Enumを使うべき理由

4-1.Enumの利点は定数の種類・数を明確にできること

前述したとおり、Enumの主な用途は定数です。定数の種類・数があらかじめ明確になっていて、しかも決められた以外の値は取りえないことが保証されているのが最大の利点です。

定数はstatic finalStringintのフィールドでも同じようなことができますが、Enumに劣っているのは値の範囲や種類を確実・明確に制限できないことです。

もう少し言葉を足すならば、Stringintでは取り得る値・表現できる範囲が広すぎることがほとんどなのです。

4-2.Enumでない定数では引数を確定・強制できない

例えば、季節をStringで以下のように定数化したとします。このStringintの広域変数による定数はC言語などではごく普通で、Javaの現場でも普通に使われます。使い方は Constant.SEASON_SPRING で、見た目上はEnumとあまり変わりません。

class Constant {
	public static final String SEASON_SPRING = "SPRING";
	public static final String SEASON_SUMMER = "SUMMER";
	public static final String SEASON_AUTUMN = "AUTUMN";
	public static final String SEASON_WINTER = "WINTER";

	public static final String RESULT_NORMAL = "100";
	public static final String RESULT_WARNING = "200";
	public static final String RESULT_ABNORMAL = "999";
}

以下のメソッドは、引数としてConstant.SEASON_SPRINGWINTERのいずれかを渡されることを前提にしていますが、他のStringを渡してもコンパイルエラーにはなりません。Javaコンパイラの観点では、Stringであれば何でもよいからです。ですから、メソッド側でのチェックが必須です。

class EnumExample {
	static void judgeSeason(String season) {
		switch (season) {
		case Constant.SEASON_SPRING:
			System.out.println("春!!");
			break;
		case Constant.SEASON_SUMMER:
			System.out.println("春!!");
			break;
		case Constant.SEASON_AUTUMN:
			System.out.println("春!!");
			break;
		case Constant.SEASON_WINTER:
			System.out.println("春!!");
			break;
		default:
			new IllegalArgumentException("想定外の値です!!");
		}
	}

	public static void main(String[] args) {
		judgeSeason(Constant.SEASON_SPRING); // メソッドで意図している呼び出し方
		judgeSeason(Constant.RESULT_ABNORMAL); // これでももちろんコンパイルは通るし、実行もできる
		judgeSeason("あいうえお"); // Stringでさえあれば、コンパイルは通ってしまうし、実行もできる
	}
}

それに、想定外の値が渡された場合でも、プログラムを動かしてみないと分かりません。一人だけでプログラムを作っているなら問題はまだ少ないのですが、複数人で作っているとこの意思疎通が地味に大変です。また、インターネットで公開され不特定多数の人が使うようなライブラリならもっと大変です。

4-3.Enumでない戻り値では呼び出し元での解釈が必要

メソッドの戻り値がEnumであることも重要です。Enumであれば、戻り値があらかじめ決められた値のいずれかであることがコンパイル時に確定するのです。

例えば、以下のプログラムでは、メソッドgetSeasonConstant.SEASON_SPRINGWINTER以外をreturnしても、コンパイルエラーにはなりません。例えば、“”だったり、“AAAAAA”だったとしてもです。戻り値がStringでさえあればいいからです。これも実行してみないと正しく実装できているか分かりません。

class EnumExample {
	static String getSeason() {
		// 何がしかの結果、Constant.SEASON_SPRING~WINTERのいずれかをreturnする仕様としている
		return ...;
	}

	public static void main(String[] args) {
		String season = getSeason(); // メソッドの戻り値のStringは、本当にConstant.SEASON_SPRING~WINTERのどれかか?

		// チェック処理をわざわざ実装する必要があるかもしれない
		switch (season) {
		case Constant.SEASON_SPRING:
			System.out.println("春!!");
			break;
		case Constant.SEASON_SUMMER:
			System.out.println("春!!");
			break;
		case Constant.SEASON_AUTUMN:
			System.out.println("春!!");
			break;
		case Constant.SEASON_WINTER:
			System.out.println("春!!");
			break;
		default:
			new IllegalArgumentException("想定外の値です!!");
		}
	}
}

引数の時と同様に、メソッドの戻り値を信用できるかどうかは使う側からは分かりません。ですので、最悪は想定通りの結果かどうかをチェックしなければならないのです。

4-4.Enumはプログラムの安全性・確実性を高める

Enumであれば、Enumで宣言した値のいずれかだとコンパイル時に確定します。Enumとしてはあらかじめ宣言していない値は使えませんし、自分でインスタンスを生成できないからです。先ほどのようにStringでやりとりするよりも、プログラムの安全性・確実性が高まることが分かるでしょうか。

また、ただのStringintの値であるよりも、Enumなら専用のデータ型であることが明確になります。定数として用いるなら、Enumの方がより望ましい姿だと言えるでしょう。

Stringやintで定数を表現するのは、他のプログラミング言語の習慣を持ち込んでいたり、JavaEnumがなかった時代の名残です。Javaの標準APIでも、Enum導入後は多くのEnumが使用されています。今Javaでプログラミングをするならば、Enumを大いに活用するのが望ましいスタイルなのです。


5.まとめ

EnumはJavaで定数を表現するために用います。Enumは宣言の仕方も使い方も簡単ですが、実体はクラスなので、やろうと思えばメソッドの実装などの様々なカスタマイズが行えるものです。

EnumがJavaに追加されたのは、従来の定数を実現する方法では、どうしてもプログラムの品質に問題が残るからです。

Enumが追加された背景や意図を理解して、効果的にEnumを使えるようになると、バグや意図しない動作を未然に防止できます。ぜひ活用していきましょう!!

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。

コメント

文系IT未経験歓迎!【23卒対象】新卒採用のエントリーを受付中!
文系IT未経験歓迎!
【23卒対象】新卒採用のエントリーを受付中!