あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

Javaで動的キャストを行う方法

はじめに

Java で動的キャストを行いたいというケースはあるかと思います。

例えば、受け取った内容によって生成するインスタンスを切り替えたいとかですね。

まずは設計を見直せと言われそうですが。。

結論から言うと、Java で動的キャストは可能ですが、以下のような警告が出てしまいます。

Unchecked cast: 'java.lang.Class<capture<?>>' to 'java.lang.Class<T>' 

この警告を消すには、 @SuppressWarnings("unchecked") をつけるしかないようです。

詳しくは下記記事を参考にしていただければと思います。

とは言え、場合によっては必要なので、Java で動的キャストをする方法を載せておきます。

実装

Web API などで、JSON を受け取って、その内容によって生成するインスタンスを切り替えるケースを考えてみます。

CastUtil というクラスに、CAST_MAP にキーとクラス名を持たせます。

cast メソッドではキーからクラス名を取得して、インスタンスを生成します。

このやり方だと、柔軟に機能を組み込むことができそうです。

public class CastUtil {

  private static final Map<String, String> CAST_MAP =
      new HashMap<>() {
        {
          put("dog", "com.example.cast.entity.Dog");
          put("cat", "com.example.cast.entity.Cat");
          put("rabbit", "com.example.cast.entity.Rabbit");
        }
      };

  public static <T> T cast(String key, String json) {

    try {
      final String type = CAST_MAP.getOrDefault(key, "");

      @SuppressWarnings("unchecked")
      final Class<T> castType = (Class<T>) Class.forName(type);

      // 指定したクラスでインスタンスを生成して返す
      final ObjectMapper objectMapper = new ObjectMapper();
      return objectMapper.readValue(json, castType);

    } catch (ClassNotFoundException | JsonProcessingException ex) {
      final String message =
          String.format("キャスト処理に失敗しました。key:%s, json:%s, message: %s", key, json, ex.getMessage());
      throw new IllegalArgumentException(message, ex);
    }
  }
}

テストクラスは以下のようになります。

class CastUtilTest {

  @Nested
  class fromJson {

    @ParameterizedTest
    @CsvSource(
        value = {
          "dog {\"type\":\"いぬ\",\"name\":\"ぽち\"} com.example.cast.entity.Dog",
          "cat {\"type\":\"ねこ\",\"name\":\"みけ\"} com.example.cast.entity.Cat",
          "rabbit {\"type\":\"うさぎ\",\"name\":\"まろん\"} com.example.cast.entity.Rabbit"
        },
        delimiterString = " ")
    void keyに対して正しいクラスでキャストされること(String key, String json, String type) {

      // 実行
      final Object actual = CastUtil.cast(key, json);

      // 検証
      Assertions.assertThat(actual.getClass().getName()).isEqualTo(type);
    }

    @ParameterizedTest
    @CsvSource(
        value = {"sheep {\"type\":\"ひつじ\",\"name\":\"めぇ\"} com.example.cast.entity.Sheep"},
        delimiterString = " ")
    void 存在しないkeyが指定された場合は例外が発生すること(String key, String json, String type) {

      // 実行&検証
      Assertions.assertThatThrownBy(() -> CastUtil.cast(key, json))
          .isInstanceOf(IllegalArgumentException.class)
          .hasMessageStartingWith(
              "キャスト処理に失敗しました。key:sheep, json:{\"type\":\"ひつじ\",\"name\":\"めぇ\"}, message:");
    }
  }
}

おわりに

Java で動的キャストはできなくはないという感じでしょうか。

まあ、@SuppressWarnings の使用が禁止されているプロジェクトも多いでしょうし、その場合は設計を見直すしかないでしょうね。