キャスレーコンサルティング 技術ブログ

再帰的ジェネリクスを活用したJava8時代のコード定義Enum用インタフェースを作成してみた

Posted on 05月 10, 2017

こんにちは。 キャスレーコンサルティング SI(システム・インテグレーション)部の満石です。

今年の7月にJava SE 9のリリースが迫っていますが、ちょっとここでJava SE 8の新機能を思い出してみましょう。

Lambda式、Stream API、Date and Time API。

これらは、有名なので実際に使ってみたことがある人は多いと思います。

でも、インタフェースにデフォルト実装とstaticメソッドが書けるようになった、というのは忘れている人や
知っていても使い所が分からないという人は多いのではないでしょうか?

今回は、そんなインタフェースのデフォルト実装とstaticメソッドについて、コード定義Enum用インタフェースの作成を
通して実用的な例を紹介したいと思います。

Java SE 7まで

アプリケーションを作成する場合、必ずと言っていいほど「コード値」に対する「名称」の組み合わせ、
所謂コード定義を扱うことになると思います。

コード定義はDBに保存したり、Enumを作成することがほとんどではないでしょうか。

コード定義用Enumを用意する場合、Java SE 7までだとどんな実装になるかやってみます。

コード定義用Enumからコード値と名称を取得したいのでまず以下のようなインタフェースを作成することにします。

public interface CodeEnum {
    String getCode();
    String getName();
}

このインタフェースを実装したEnumとして、今回は個人的な趣味で今年のF1のコンストラクターを
題材にしたEnumを作成しました。
アルファベット順にコード値を振り、カタカナの名称を持たせました。

public enum F1Constructor implements CodeEnum {
    FERRARI("01", "フェラーリ"),
    FORCE_INDIA("02", "フォース・インディア"),
    HAAS("03", "ハース"),
    MCLAREN("04", "マクラーレン"),
    MERCEDES("05", "メルセデス"),
    RED_BULL("06", "レッドブル"),
    RENAULT("07", "ルノー"),
    SAUBER("08", "ザウバー"),
    TORO_ROSSO("09", "トロ・ロッソ"),
    WILLIAMS("10", "ウィリアムズ");

    private String code;

    private String name;

    F1Constructor(String code, String name) {
        this.code = code;
        this.name = name;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }
}

このようなコード定義用Enumを作成すると、例えばコード値を元に指定したEnumの列挙子を作成するような
共通処理をするメソッドを親クラスに実装して継承したくなります。

しかし、Enumは特殊なクラスなので、上記のようにインタフェースを実装することは出来ても、
親クラスを継承することは出来ません。

かと言って、個別のEnumにほとんど同じ実装のメソッドを書くのは得策ではありません。

そのため、コード定義Enum専用のユーティリティクラスを作成し、コード定義Enum用の共通処理は
そのユーティリティクラスに実装したりすることが多かったのではないかと思います。

Java SE 8から

Java SE 8からはインタフェースにデフォルト実装やstaticメソッドが書けるため、
独自のユーティリティクラスで実現していたことを、すべてEnum用のインタフェースだけで実現できます。

Enumは特殊なクラスと言いましたが、Enum自身の型宣言はEnum<E extends Enum<E>>となっていて、
再帰的ジェネリクスが使われています。今回作成するEnum用インタフェースはこれも活用します。

コード定義用Enumのインタフェースとして実用的なメソッドを考慮した結果、以下の実装になりました。
Lambda式、Stream API、メソッド参照もフル活用しています。

public interface CodeEnum<E extends Enum<E>> {

    /** コード値を返却する */
    String getCode();

    /** 名称を返却する */
    String getName();

    /** [デフォルト実装] 表示の順番を返却する */
    default int getOrder() {
        return Integer.parseInt(getCode());
    }

    /** [デフォルト実装] Enumに変換する */
    @SuppressWarnings("unchecked")
    default E toEnum() {
        return (E) this;
    }

    /** [デフォルト実装] コード値が同一かどうかをチェックする */
    default boolean equalsByCode(String code) {
        return getCode().equals(code);
    }

    /** [staticメソッド] 指定されたCodeEnumを実装したEnumを表示順にソートしたリストを返却する */
    static <E extends Enum<E>> List<E> getOrderedList(Class<? extends CodeEnum<E>> clazz) {
        return Arrays.stream(clazz.getEnumConstants())
                .sorted(Comparator.comparing(CodeEnum::getOrder))
                .map(CodeEnum::toEnum)
                .collect(Collectors.toList());
    }

    /** [staticメソッド] 指定されたCodeEnumを実装したEnumの、指定されたコード値の列挙子を返却する */
    static <E extends Enum<E>> E getEnum(Class<? extends CodeEnum<E>> clazz, String code) {
        return Arrays.stream(clazz.getEnumConstants())
                .filter(e -> e.equalsByCode(code))
                .map(CodeEnum::toEnum)
                .findFirst()
                .orElse(null);
    }

    /** [staticメソッド] 指定されたCodeEnumのコード値をキー、コード値に該当するCodeEnumを値に持つMapを返却する */
    static <E extends Enum<E>> Map<String, E> getMap(Class<? extends CodeEnum<E>> clazz) {
        return Arrays.stream(clazz.getEnumConstants())
                .collect(Collectors.toMap(CodeEnum::getCode, CodeEnum::toEnum));
    }

    /** [staticメソッド] 指定されたCodeEnumに、指定されたコード値を持つ列挙子が存在するかチェックする */
    static <E extends Enum<E>> boolean hasCode(Class<? extends CodeEnum<E>> clazz, String code) {
        return Arrays.stream(clazz.getEnumConstants())
                .anyMatch(e -> e.equalsByCode(code));
    }
}

再帰的ジェネリクスを、シンプルな形で活用しているのはtoEnum()メソッドです。
再帰的ジェネリクスはこのメソッドのように、サブクラス(※ここではEnum)の型を扱いたい場合に使うと便利です。

もっと深く知りたい人は、「再帰的ジェネリクス」で検索してみてください。
toEnum()メソッドになぜ@SuppressWarnings("unchecked")が必要なのか、きっと分かるはずです。

このインタフェースを実装すると、F1Constructorはこうなります。

public enum F1Constructor implements CodeEnum<F1Constructor> {
    FERRARI("01", "フェラーリ"),
    FORCE_INDIA("02", "フォース・インディア"),
    HAAS("03", "ハース"),
    MCLAREN("04", "マクラーレン"),
    MERCEDES("05", "メルセデス"),
    RED_BULL("06", "レッドブル"),
    RENAULT("07", "ルノー"),
    SAUBER("08", "ザウバー"),
    TORO_ROSSO("09", "トロ・ロッソ"),
    WILLIAMS("10", "ウィリアムズ");

    private String code;

    private String name;

    F1Constructor(String code, String name) {
        this.code = code;
        this.name = name;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }
}

違いは、再帰的ジェネリクス特有の記述となっている<F1Constructor>の部分だけなので、活用するのはとても簡単です。

サンプルコードと実行結果

ここからは、F1Constructorを使ったサンプルコードと、その実行結果から動作を確認してみます。
デフォルト実装しているメソッドのgetOrder()toEnum()は、CodeEnumインタフェース内のstaticメソッドで
使うことが主目的なので、ここではequalsByCode()と4つのstaticメソッドについて確認していきます。

equalsByCodeメソッド

コード定義のEnumを作成した場合、特定のEnumの列挙子のコード値と指定したコード値が一致するか
確認したいことが良くあると思います。そのような場合、このメソッドだと簡潔に記述することが出来ます。

public class Main {
    public static void main(String[] args) {
        System.out.println(F1Constructor.FERRARI.equalsByCode("01"));
    }
}

実行結果は

true

になります。

getOrderedListメソッド

コード定義の全種類をコード値順にプルダウンリストとして表示したい場合、
その対象データを作成する目的で作成しました。

public class Main {
    public static void main(String[] args) {
        CodeEnum.getOrderedList(F1Constructor.class)
                .forEach(e -> System.out.println("code:" + e.getCode() + ",name:" + e.getName()));
    }
}

実行結果は

code:01,name:フェラーリ
code:02,name:フォース・インディア
code:03,name:ハース
code:04,name:マクラーレン
code:05,name:メルセデス
code:06,name:レッドブル
code:07,name:ルノー
code:08,name:ザウバー
code:09,name:トロ・ロッソ
code:10,name:ウィリアムズ

になります。

ここで遊び心を入れて、2017年第4戦終了時のコンストラクターズポイントランキング順を持つようにしてみました。

public enum F1Constructor implements CodeEnum<F1Constructor> {
    FERRARI("01", "フェラーリ", 2),
    FORCE_INDIA("02", "フォース・インディア", 4),
    HAAS("03", "ハース", 7),
    MCLAREN("04", "マクラーレン", 10),
    MERCEDES("05", "メルセデス", 1),
    RED_BULL("06", "レッドブル", 3),
    RENAULT("07", "ルノー", 8),
    SAUBER("08", "ザウバー", 9),
    TORO_ROSSO("09", "トロ・ロッソ", 6),
    WILLIAMS("10", "ウィリアムズ", 5);

    private String code;

    private String name;

    private int order;

    F1Constructor(String code, String name, int order) {
        this.code = code;
        this.name = name;
        this.order = order;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getOrder() {
        return order;
    }
}

orderのフィールドを追加して順位が入るようにし、デフォルト実装のgetOrder()メソッドをオーバーライドし、
orderの値を返却するようにしたのが変更点です。

再度サンプルコードを実行すると、結果は以下のとおりに変わります。

code:05,name:メルセデス
code:01,name:フェラーリ
code:06,name:レッドブル
code:02,name:フォース・インディア
code:10,name:ウィリアムズ
code:09,name:トロ・ロッソ
code:03,name:ハース
code:07,name:ルノー
code:08,name:ザウバー
code:04,name:マクラーレン

このように、必要に応じて表示順を変更することも出来ます。もしコード値にアルファベットが混じっているなら、
デフォルト実装では実行時にエラーとなるため、この方法で表示順を指定することでエラーを回避できます。

getMapメソッド

コード定義用Enumを作る場合、コード定義用Enumのコード値をキー、
対応するコード定義用Enumの列挙子を値に持つMapを使いたくなるシチュエーションがあると思います。
このメソッドなら、簡単にそのようなMapを取得することが出来ます。

public class Main {
    public static void main(String[] args) {
        CodeEnum.getMap(F1Constructor.class)
                .entrySet()
                .forEach(e -> System.out.println("code:" + e.getKey() + ",Enum:" + e.getValue()));
    }
}

実行結果は

code:01,Enum:FERRARI
code:02,Enum:FORCE_INDIA
code:03,Enum:HAAS
code:04,Enum:MCLAREN
code:05,Enum:MERCEDES
code:06,Enum:RED_BULL
code:07,Enum:RENAULT
code:08,Enum:SAUBER
code:09,Enum:TORO_ROSSO
code:10,Enum:WILLIAMS

となります。

hasCodeメソッド

入力されたコード値がコード定義として存在する値なのか、チェックしたい場合に使うことを想定して作りました。

public class Main {
    public static void main(String[] args) {
        System.out.println(CodeEnum.hasCode(F1Constructor.class, "10"));
    }
}

実行結果は

true

になります。

終わりに

インタフェースのデフォルト実装を使うことで、これまで不可能だったEnum自身に機能を追加することが
可能になったのは新鮮な驚きでした。
それなら自分はこんなメソッドを実装したい、と考えた人もいらっしゃるのではないでしょうか。

そして、インタフェースにstaticメソッドを実装できるようになったため、
今回はコード定義用Enumに対する実装を、1つのクラスに集約することができました。
ちょっとしたことですが、「1つのクラスに必要な処理が全て揃っている」というのは、
使い方を説明する側・される側双方にメリットになると思います。

それから、再帰的ジェネリクス。実際に活用してみたのは初めてでしたが、
ジェネリクスに対する理解がさらに深まりました。

今回紹介したコード定義Enum用インタフェースを、
皆さんが作成するアプリケーションでも応用していただけたら幸いです。


採用情報

  • Profile
    キャスレーコンサルティングの技術ブログです。
    当社エンジニアが技術面でのTips、技術系イベント等についてご紹介いたします。
  • CSV社長ブログ
  • チーム・キャスレーブログ