こんにちは、かつコーチです。
「曜日」や「ステータス」のような決まった値を扱うとき、intやStringの定数で済ませていませんか。
今回はJavaのenum(列挙型)を使って、そうしたコードを安全で読みやすくする方法を解説します。
enumとは?定数管理に潜む問題を解決する仕組み
enum(列挙型)の基本定義
enum(あらかじめ決まった値の集合を表す特別なクラス)は、次のように定義します。
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
Season.SPRINGのように定数として使え、switch文とも自然に組み合わせられます。
Season season = Season.SUMMER;
switch (season) {
case SPRING -> System.out.println("春です");
case SUMMER -> System.out.println("夏です");
case AUTUMN -> System.out.println("秋です");
case WINTER -> System.out.println("冬です");
}
なぜint定数ではなくenumを使うのか
Java 5以前によく見られたのが、int定数で状態を表す方法です。
// ❌Before:int定数で状態を表す
public static final int STATUS_PENDING = 0;
public static final int STATUS_APPROVED = 1;
public static final int STATUS_REJECTED = 2;
public void updateStatus(int status) {
if (status == STATUS_APPROVED) {
// 承認処理
}
}
この書き方には2つの問題があります。
1つ目は、updateStatus(99)のような不正な値を渡してもコンパイルエラーにならないこと。
2つ目は、STATUS_APPROVEDが実は数値の1に過ぎず、意味のない数値と誤って比較してもコンパイラが検出できないことです。
// ✅After:enumで状態を表す
public enum Status {
PENDING, APPROVED, REJECTED
}
public void updateStatus(Status status) {
if (status == Status.APPROVED) {
// 承認処理
}
}
enumにすることで、取り得る値がコンパイル時に保証され、updateStatus(Status.APPROVED)以外の呼び出し方が型レベルで排除されます。
enumにフィールド・メソッドを持たせる
コンストラクタとフィールドを持つenum
enumは実はクラスの一種なので、フィールドやコンストラクタ、メソッドを持てます。
public enum Planet {
MERCURY(3.303e+23, 2.4397e6),
VENUS(4.869e+24, 6.0518e6),
EARTH(5.976e+24, 6.37814e6);
private final double mass;
private final double radius;
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
public double surfaceGravity() {
final double g = 6.67300E-11;
return g * mass / (radius * radius);
}
}
for (Planet planet : Planet.values()) {
System.out.printf("%s の重力: %.2f%n", planet, planet.surfaceGravity());
}
Planet.values()は全定数を配列として返すため、ループ処理にも使いやすいです。
定数ごとに振る舞いを変える(定数固有メソッド)
さらに一歩進めると、定数ごとに異なる実装を持たせることもできます。
public enum Operation {
ADD {
@Override
public int apply(int a, int b) {
return a + b;
}
},
SUBTRACT {
@Override
public int apply(int a, int b) {
return a - b;
}
};
public abstract int apply(int a, int b);
}
int result = Operation.ADD.apply(3, 4); // 7
ifやswitchの分岐を書かずに、定数自身が振る舞いを持つ設計です。
はじめて見たときは書き方に戸惑いましたが、「分岐をなくす」という観点ではポリモーフィズムの応用として理解できました。
enumとインターフェースの組み合わせ
interfaceを実装するenum
enumはinterfaceを実装できます。
複数のenumで共通の振る舞いを持たせたいときに便利です。
public interface Discountable {
double discountRate();
}
public enum MemberRank implements Discountable {
BRONZE(0.0),
SILVER(0.05),
GOLD(0.1);
private final double rate;
MemberRank(double rate) {
this.rate = rate;
}
@Override
public double discountRate() {
return rate;
}
}
Discountable型の変数として扱えるため、enumであることを意識せずにメソッドを呼び出せます。
EnumMap・EnumSetで効率よく扱う
enumをキーにするなら、通常のHashMapよりEnumMapを使う方が効率的です。
内部的に配列でデータを保持するため、ハッシュ計算のオーバーヘッドがありません。
EnumMap<Season, String> events = new EnumMap<>(Season.class);
events.put(Season.SPRING, "花見");
events.put(Season.SUMMER, "花火大会");
つまずいたポイント:valueOf()のIllegalArgumentException
文字列からenumへの変換で起きたエラー
外部APIから受け取ったステータス文字列をenumに変換する処理で、想定外の値が来て例外が発生したことがあります。
Status status = Status.valueOf(input); // inputが"unknown"だと例外発生
Exception in thread "main" java.lang.IllegalArgumentException:
No enum constant com.example.Status.unknown
IllegalArgumentException(メソッドに不正な引数が渡されたときにスローされる例外)が発生する原因は、valueOf()が完全一致でしか変換できないためでした。
大文字小文字の違いすら許容されません。
対処法として、Optionalを使った安全な変換メソッドを用意しました。
public static Optional<Status> fromString(String value) {
return Arrays.stream(Status.values())
.filter(s -> s.name().equalsIgnoreCase(value))
.findFirst();
}
外部入力をenumに変換する処理では、valueOf()を直接使わずワンクッション挟むようにしています。
まとめ
この記事のポイント
enumはint定数やString定数に比べて、コンパイル時に不正な値を防げるenumはクラスの一種であり、フィールド・コンストラクタ・メソッドを持てる- 定数ごとに異なる実装を持たせることで、分岐処理をなくせる
enumはinterfaceを実装でき、EnumMapと組み合わせると効率よく扱えるvalueOf()は完全一致でしか変換できず、外部入力を扱う際は安全な変換処理を用意する
次に読むべき記事
- 定数固有メソッドの応用にもつながる「メソッドのオーバーライドとポリモーフィズム」の記事へ
- Java 21の新機能を扱う「record(Java21)でシンプルにデータを表現する」の記事へ
タグ: Java, 中級者向け, オブジェクト指向