JavaのEnumでコード値からの逆引きが欲しくなるときはあるけど、本当に必要ですか?

5 min read読了の目安(約5000字

はじめに

Qiitaとダブルポストです。
やや煽り気味のタイトルですが、必要なときは当然あります。
けど、実際は、「逆引きは使わずにすむならその方がいい」というのが、多くの人が感じているところでしょう。

この記事では以下を主な目的としています。

  • 極力使わないようにするやり方
  • 使わざるを得ないケースの整理
  • 使う場合でも、使用箇所を減らすやり方

Enumの逆引きとは

コード値(数値や特定の文字列)をプログラム上で扱うときに、Javaではenumを使うことが多いです。その場合、コード値から対応するenumの要素に変換したいケースもよくあります。このことをenumの逆引き(lookup)といわれることが多いです。

主な方針としては、「enum要素全体からコード値が一致するもの探索」か「コード値をキーとしたマップを作成」の2つがあります。

Javaではstaticメソッドを共通化する仕組みがないので、enumを作るごとに、この逆引きのメソッドを定義することになります。毎回同じようなものを定義するのがややスマートにみえないので、ユーティリティメソッドの形で一箇所にまとめる方針もあります。

逆引きを共通化するときの懸念点

わたされた値に対応するenumの要素がないときに、どうするべきかが、状況によって違うことです。

  1. (後続の処理に必須な値などで)例外スローする
  2. 適当な(nullでない)初期値を返す
  3. 一概には決められないので保留したい(Optionalを返す)

現状のJavaのOptional周りの機能を考えると、Enumを使っているなら、例外スローか適当な初期値にした方が取り回しがいいです。

そうすると、ユーティリティメソッドではOptionalを返して、呼び出し側で、例外スローか、初期値にする、ということになります。それなら、それぞれのenumのクラスで、状況にあった逆引きメソッドをつくった方が、そのenumをどう使うべきかをわかりやすくなると思います。

Stream APIを使えば、逆引きメソッドは4,5行もあればできます。(メソッドチェーンによる改行なので、実質1行です。)


enum UserStatus {
   UNKNOWN(null),
   ACTIVE(1),
   INACTIVE(-1),
   PENDING(0),

   private final Integer value;

   UserStatus(Integer value) {
      this.value = value;
   }

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

   // コード値からの逆引き
   public static UserStatus from(Integer value) {
       return Arrays.stream(UserStatus.values)
                    .filter(userStatus.getValue() == value)
                    .findFirst()
                    .orElse(UNKNOWN);
                       
   }
 
}

言語の標準機能で数行でかけるものを、共通化メソッドにすることはたいていはあまりメリットはないです。読む側からすると、宣言箇所へのコードジャンプが多いほど、理解するためのコストが増えます。

そもそも逆引きが必要になるケースとは

コード値からenumへの変換が必要なケースを整理してみましょう。
enumはvalueOfという、要素名からenumに変換するメソッドがあります。フレームワークによってはこれを使った変換をやってくれるケースがあります。thymeleafでの例はこちらになります。

このvalueOfを使えば、自分たちが作ったwebアプリでは、入力画面などのフォームには、enumの要素名を送信させるようにつくれば、逆引きは実質不要です。もっというと、コード値を露出させずに、enumの要素名を使えるケースでは逆引きは不要といえます。

そうなると、逆説的には必要なケースは、コード値そのものを扱わざるを得ないことになります。

  • DBから取得するとき
  • REST APIなどで、コード値でのリクエストを受け付けるとき

ようは、「外部と連携するとき、連携先からコード値が送られてくる」といえそうです。
この場合は逆引きメソッドを用意せざるを得ないです。フレームワークをうまく使うと、逆引きを使う箇所をおさえられます。

例えば、JPAではAttributeConverterという仕組みがあります。

https://www.baeldung.com/jpa-attribute-converters

public class UserStatusConverter implements AttributeConverter<UserStatus, Integer> {
   
   @Override
   public Integer convertToDatabaseColumn(UserStatus userStatus) {
          return userStatus.getValue();
   }

   @Override
   public UserStatus convertToEntityAttribute(Integer value) {
          return UserStatus.from(value);
   }

  
}


@Entity
public class User {

   private long id;

   @Converter(converter = UserStatusConverter.class) 
   private UserStatus status;

} 

DBにenumの要素名か、ordinal()の値(0始まりの定義した順番)で保存できる場合は、@Enumerated(EnumType.STRING)@Enumerated(EnumType.ORDINAL) のアノテーションをつけることで、enumへの変換をやってくれます。
https://www.baeldung.com/jpa-persisting-enums-in-jpa が参考になります。

新規プロジェクトの場合、コード値を数値で持つことにこだわらず、enumのの要素名で定義できないかを検討した方がいいかもしれません。

MyBatisやJdbcTemplateのような、sqlの結果行をオブジェクトにマッピングする場合、そのマッピングのところで逆引きメソッドを使います。

JdbcTemplateの場合のサンプルです。ResultSetからのstatic factory methodを用意すると便利です。


public class User {

  public static User of(ResultSet rs) throw SqlException {
       return new User(
               rs.getLong("id"),
               ...
               UserStatus.from(rs.getInt("status"))   
              );
  } 
}

 @Autowired
 private JdbcTemplate jdbcTemplate;

 public List<User> getAll() {
      String sql = "select * from user";
      return jdbcTemplate.query(sql, (rs, rowInt) -> User.of(rs));  
 }

そもそもなんでEnumを使いたいのか

定数をまとめて管理したいだけなら、定数クラスでも十分です。コード値をEnumで表現したい背景として、コード値に応じて、処理をかえるなど、なんらかのビジネスロジックが関わってくるからです。
そのため、コード値に対して具体的な操作をうまく隠蔽することと、コード値全体をまとめて管理する機能が欲しくなります。enumはclassでもあり、定義した要素がシングルトンインスタンスとして生成されるので、この2つの要求をある程度みたしてくれます。

それを踏まえると、コード値は極力露出させないことが、enumを使う上で重要だと思います。極論をいえば、コード値を全く出さない(あるいは、その部分はフレームワークにおしつける)のが正解です。
不用意にコード値を露出してしまうと、頻繁にenumとコード値との変換を繰り返すことになり、可読性を下げてしまいます。コード値を使うのは、DBに登録するときなど、必要になる最後のタイミングだけにし、それまでは極力enumのままにしておくといいです。

参考サイト