テックブログ

Java Enum入門:文字列定数のミスを減らす使い方

Java Enum入門:文字列定数のミスを減らす使い方

JavaのEnumは、ステータスや種別のように取り得る値が決まっているものを、型として表現する仕組みです。文字列や数値だけで区分値を管理すると、タイプミスや不正な値が混ざりやすくなります。Enumを使うことで、決められた候補だけを扱いやすくなり、注文状態、会員種別、権限区分などのコードを読みやすく、安全に管理しやすくなります。DBやAPI変換時には、表示名と内部コードを別々に考慮する必要があります。さらに、変換失敗時の取り扱いも分けて検討してください。

1. Enumとは何か

1-1. 決まった候補を型として表す仕組み

JavaのEnumは、決まった候補の中から値を選ぶための型です。ステータス、種別、区分、権限のように、取り得る値があらかじめ決まっているものを表すときに使います。

たとえば、注文ステータスには「未処理」「支払い済み」「発送済み」「キャンセル済み」のような候補があります。このような値を単なる文字列で扱うと、どの文字列が有効なのかがコード上で分かりにくくなります。Enumにすると、候補を1つの型として定義でき、存在しない値を扱いにくくできます。

public enum OrderStatus {
    NEW,
    PAID,
    SHIPPED,
    CANCELED
}

このコードでは、注文ステータスとして使える値をOrderStatus型にまとめています。注意点は、Enumを単なる定数一覧として見るのではなく、「この型には決められた候補だけが入る」と表現できる点に価値があることです。

1-2. 文字列定数との違い

Enumと文字列定数の大きな違いは、コンパイル時に型としてチェックできるかです。文字列定数では、不正な文字列を渡してもコンパイルエラーにならないことがあります。

文字列定数では、"PAID""SHIPPED"のような値を使います。しかし、タイプミスで"PADI"と書いても、Javaとしてはただの文字列なのでコンパイルできます。一方、EnumではOrderStatus.PAIDのように候補を型で指定するため、存在しない値を書けばコンパイル時に気づけます。

// 文字列定数の例
String status = "PADI"; // タイプミスでもコンパイルできる

// Enumの例
OrderStatus status = OrderStatus.PAID; // 定義済みの候補から選ぶ

このコードでは、文字列ではタイプミスが混ざりやすい一方、Enumでは定義済みの値を使う形になります。注意点は、外部から受け取るAPI値やDB値は文字列で来ることが多いため、Enumへ変換する境界部分の設計も必要になることです。

1-3. ステータスや種別管理で使いやすい理由

Enumは、ステータスや種別のような区分値を管理する場面で使いやすいです。候補が決まっていて、処理分岐にも使われる値を明確に表現できるからです。

実務では、注文状態、ユーザー種別、支払い方法、通知種別、権限ロールなど、決まった候補を扱う場面が多くあります。これらを文字列やintだけで管理すると、値の意味が分かりにくく、不正値も混ざりやすくなります。Enumを使うと、候補の一覧と意味をコード上に集約できます。

たとえば、UserType.ADMINと書かれていれば、管理者ユーザーを表していることが読み取りやすくなります。"1"1だけでは意味が分かりにくいため、コードレビューや保守で確認コストが増えます。区分値に意味を持たせたい場合は、Enumを検討しましょう。

2. 文字列定数で起きる問題

2-1. タイプミスが混ざる

文字列定数で区分値を管理すると、タイプミスが実行時まで見つかりにくいという問題があります。Javaは文字列の中身まではコンパイル時に候補としてチェックしてくれません。

たとえば、注文ステータスを"PAID"という文字列で扱う場合、"PAID""paid""PADI"のような表記ゆれが混ざる可能性があります。これらはすべてStringとしては正しいため、コンパイルエラーにはなりません。結果として、条件分岐に入らない、集計から漏れる、想定外ステータスとして扱われるといった問題が起きます。

// 悪い例:文字列でステータスを直接扱う
String status = "PADI";

if ("PAID".equals(status)) {
    System.out.println("支払い済みです");
}

このコードでは、"PAID"のつもりで"PADI"と書いてもコンパイルできます。注意点は、テストデータでは偶然正しく動いても、別の入力や手修正で表記ゆれが入る可能性があることです。候補が決まっているなら、文字列を直接扱う範囲を減らすべきです。

2-2. 不正な値を渡せてしまう

文字列やintで区分値を扱うと、本来あり得ない値でもメソッドに渡せてしまうことがあります。これは型で制限できていない状態です。

たとえば、注文ステータスをStringで受け取るメソッドは、"PAID"だけでなく、"UNKNOWN"や空文字、まったく関係ない文字列も受け取れてしまいます。intで管理する場合も、12以外の999を渡せてしまいます。候補を型として表していないため、呼び出し側のミスを防ぎにくくなります。

// 悪い例:どんな文字列でも渡せてしまう
public void changeStatus(String status) {
    // statusが有効な値かどうかは中で確認する必要がある
}

changeStatus("UNKNOWN");

このコードでは、注文ステータスとして不正な値でも渡せます。注意点は、メソッドの入口ごとに値チェックが必要になり、チェック漏れが起きやすいことです。Enumを引数にすれば、少なくともJavaコード内部では定義済みの候補に絞りやすくなります。

2-3. どの値が有効なのか分かりにくい

文字列定数だけで管理すると、どの値が有効な候補なのかを探しにくくなることがあります。候補がコードのあちこちに散らばると、保守性が下がります。

区分値は、条件分岐、DB保存、APIレスポンス、画面表示など複数の場所で使われます。文字列を直接書いていると、候補の一覧がどこにあるのか分かりにくくなり、新しい値を追加するときの影響範囲も追いづらくなります。Enumにまとめれば、候補の一覧を1つの型として確認できます。

たとえば、支払い方法にCREDIT_CARDBANK_TRANSFERCASHがある場合、Enumを見れば候補を把握できます。文字列が複数クラスに散らばっていると、新しいQR_PAYMENTを追加するときに見落としが起きやすくなります。

3. Enumの基本的な使い方

3-1. Enumの定義

Enumは、enumキーワードを使って定義します。候補となる値をカンマ区切りで並べるのが基本です。

Enumの値は、通常大文字のスネークケースで書かれることが多いです。たとえば、注文ステータスならNEWPAIDSHIPPEDのように定義します。利用側ではOrderStatus.PAIDのように、Enum型名と値名を組み合わせて参照します。

public enum UserType {
    GENERAL,
    ADMIN,
    GUEST
}

UserType userType = UserType.ADMIN;

このコードでは、ユーザー種別をEnumとして定義しています。注意点は、Enum名や値名は内部コードとして使われることがあるため、あとから安易に変更するとDB値やAPI値との変換に影響する場合があることです。

3-2. switchやifでの判定

Enumは、ifswitchで分岐しやすい値です。ステータスごとに処理を変える場面で読みやすく書けます。

文字列で分岐する場合は、表記ゆれやタイプミスに注意が必要です。Enumであれば、候補が型として決まっているため、分岐対象が明確になります。また、switchを使うと、ステータスごとの処理を一覧しやすくなります。

public String getStatusMessage(OrderStatus status) {
    return switch (status) {
        case NEW -> "注文を受け付けました";
        case PAID -> "支払いが完了しました";
        case SHIPPED -> "発送しました";
        case CANCELED -> "キャンセルされました";
    };
}

このコードでは、注文ステータスごとに表示メッセージを返しています。注意点は、Enumの値を追加した場合に、関連するswitchの見直しが必要になることです。追加した値に対する処理を忘れると、想定外の動作につながります。

3-3. values() と valueOf() の基本

values()はEnumの全候補を取得し、valueOf()文字列からEnum値へ変換するためのメソッドです。ただし、valueOf()は安全な変換方法として無条件に使うものではありません。

values()は、画面の選択肢を作る場合や、Enumの候補を一覧したい場合に使えます。valueOf()は、Enum名と完全一致する文字列をEnumへ変換します。しかし、一致しない文字列を渡すとIllegalArgumentExceptionが発生します。外部入力をそのままvalueOf()に渡すと、想定外の値で処理が落ちる可能性があります。

for (OrderStatus status : OrderStatus.values()) {
    System.out.println(status);
}

OrderStatus status = OrderStatus.valueOf("PAID");

このコードでは、Enumの全候補を表示し、文字列からOrderStatus.PAIDへ変換しています。注意点は、valueOf("paid")valueOf("UNKNOWN")は失敗することです。APIやDBの値を変換する場合は、後述するコード値変換のように失敗時の扱いを明確にしましょう。

4. 表示名やコード値を持たせる

4-1. フィールドとコンストラクタを持つEnum

Enumには、フィールドやコンストラクタ、メソッドを持たせることができます。これにより、単なる候補名だけでなく、表示名やコード値も一緒に管理できます。

実務では、Java内部ではPAIDとして扱いたいが、画面には「支払い済み」と表示したい、DBには"02"として保存したい、ということがあります。このような場合、Enumに表示名やコード値を持たせると、関連情報を1か所にまとめられます。

public enum OrderStatus {
    NEW("01", "新規注文"),
    PAID("02", "支払い済み"),
    SHIPPED("03", "発送済み"),
    CANCELED("04", "キャンセル済み");

    private final String code;
    private final String displayName;

    OrderStatus(String code, String displayName) {
        this.code = code;
        this.displayName = displayName;
    }

    public String code() {
        return code;
    }

    public String displayName() {
        return displayName;
    }
}

このコードでは、Enum値ごとにコード値と表示名を持たせています。注意点は、表示名やコード値を追加すると、Enumが外部仕様に近づくことです。DBやAPIで使う値を変更すると互換性に影響するため、内部名と外部コードを分けて考えましょう。

4-2. 画面表示用の名前を持たせる例

画面表示用の名前は、Enum名とは別に持たせるのが基本です。Enum名をそのまま画面に出すと、利用者向けの表現として不自然になることがあります。

Enum名はJavaコード内で扱いやすい名前にすることが多く、PAIDSHIPPEDのように英語の内部名になります。一方、画面には「支払い済み」「発送済み」のような日本語の表示名を出したいことがあります。表示名をEnumのフィールドとして持たせると、画面側で変換しやすくなります。

OrderStatus status = OrderStatus.PAID;

System.out.println(status.displayName()); // 支払い済み

このコードでは、内部的にはOrderStatus.PAIDを使い、画面表示にはdisplayName()を使っています。注意点は、多言語対応が必要な場合です。日本語、英語などを切り替える要件があるなら、Enumに固定文字列を持たせるより、メッセージリソースで管理する設計が向くこともあります。

4-3. DB/API用のコード値を持たせる例

DBやAPIと連携する場合は、Enum名ではなく、外部仕様として安定したコード値を持たせると扱いやすくなります。Enum名をそのまま保存・公開すると、名前変更の影響が大きくなることがあります。

DBには"01""02"のようなコード値を保存し、Java内部ではOrderStatus.NEWOrderStatus.PAIDとして扱う設計があります。APIでも、外部仕様として"paid"のような値を返し、Java内部のEnum名とは分けることがあります。この分離により、内部の読みやすさと外部仕様の安定性を両立しやすくなります。

public static Optional<OrderStatus> fromCode(String code) {
    return Arrays.stream(values())
        .filter(status -> status.code().equals(code))
        .findFirst();
}

このコードでは、DBやAPIから受け取ったコード値をEnumへ変換しています。注意点は、該当するコードがない場合にOptional.empty()を返していることです。外部入力は不正値を含む可能性があるため、valueOf()で落とすのではなく、変換失敗をどう扱うかを設計しましょう。

5. よくある落とし穴

5-1. valueOfで想定外の文字列を変換して落ちる

valueOf()の落とし穴は、想定外の文字列を渡すと例外で落ちることです。外部入力の変換に無条件で使うのは危険です。

valueOf()は、Enum名と完全一致する文字列だけを変換できます。小文字、空文字、未定義の値、前後に空白がある値は失敗します。DBやAPI、フォーム入力など外部から来る値は常に正しいとは限らないため、変換失敗時に400エラーにするのか、デフォルト値にするのか、エラーとして記録するのかを決める必要があります。

// 避けたい例
OrderStatus status = OrderStatus.valueOf(input);

// 安全に扱いやすい例
Optional<OrderStatus> status = OrderStatus.fromCode(input);

このコードでは、外部入力をそのままvalueOf()に渡す例を避けています。注意点は、valueOf()自体が悪いわけではないことです。定義済みのEnum名を内部的に変換するなど、入力が確実に制御されている場面では使えますが、外部入力では失敗時の扱いを明示しましょう。

5-2. 表示名と内部コードを混同する

表示名と内部コードを混同すると、画面表示、DB保存、API仕様の変更に弱い設計になります。それぞれの目的を分けて扱うことが大切です。

表示名はユーザーに見せる文字列であり、変更される可能性があります。たとえば「支払い済み」を「入金済み」に変更することがあります。一方、DBやAPIのコード値は、既存データや外部システムとの互換性に関わるため、簡単には変えられません。内部のEnum名も、Javaコードの読みやすさのための名前です。

たとえば、DBに「支払い済み」という表示名をそのまま保存していると、文言変更時に既存データとの整合性が問題になります。DBには安定したコード値、画面には表示名、Java内部ではEnum値というように役割を分けると安全です。

5-3. Enumを増やしたときの影響範囲を確認しない

Enumに新しい値を追加したときは、関連する分岐、画面表示、DB/API変換、テストを確認する必要があります。候補を増やすだけで終わらない場合が多いです。

たとえば、注文ステータスにRETURNEDを追加した場合、画面表示、検索条件、集計処理、通知処理、APIレスポンス、DB変換、switch文などに影響する可能性があります。Enumは候補を1か所にまとめられる反面、その候補を使う処理は複数箇所に広がることがあります。

新しいEnum値を追加したら、switchで未対応の分岐がないか、表示名やコード値を設定したか、外部仕様として公開してよいかを確認しましょう。単にコンパイルが通るだけでなく、業務上の扱いが決まっているかを見ることが重要です。

6. まとめ

6-1. Enumを使うべき場面

Enumを使うべき場面は、取り得る値が決まっていて、その候補以外を扱いたくない場合です。ステータス、種別、区分、権限、支払い方法のような値に向いています。

文字列やintだけで区分値を管理すると、タイプミス、不正値、意味の分かりにくさが問題になります。Enumを使うと、候補を型として表現でき、Javaコード内部では決められた値だけを扱いやすくなります。さらに、表示名やコード値を持たせることで、画面、DB、APIとの対応も整理できます。

ただし、Enumは万能ではありません。外部から来る文字列の変換、DB/APIコードとの対応、値を追加したときの影響範囲は必ず考える必要があります。特にvalueOf()を外部入力に直接使う場合は、変換失敗時の扱いに注意しましょう。

6-2. 区分値管理のチェックリスト

Enumで区分値を管理するときは、候補・変換・表示・保存の影響を確認します。追加時の影響も確認すると、失敗を減らせます。

  • 取り得る値があらかじめ決まっているか
  • 文字列やintで不正値を渡せる設計になっていないか
  • Enum名、表示名、DB/APIコード値を分けて考えているか
  • 外部入力をvalueOf()へ直接渡していないか
  • 変換失敗時にOptional.empty()、例外、エラーレスポンスのどれで扱うか決めているか
  • 画面表示用の文言を内部コードとして保存していないか
  • Enum値を追加したときにswitchや集計処理を見直しているか
  • DBやAPIの既存データとの互換性を確認しているか

Enumを正しく使うと、文字列定数にありがちなタイプミスや不正値を減らし、区分値の意味をコード上で読み取りやすくできます。重要なのは、候補をEnumにまとめるだけでなく設計することです。外部との変換や表示名の違いも含めて考えます。

7. 参考リンク