デトフィア

プログラミング

Javaのイマジナリージェネリクス

ModelMapperにある、とある処理

private <D> D mapInternal(Object source, D destination, Type destinationType, String typeMapName) {
if (destination != null)
  destinationType = Types.<D>deProxy(destination.getClass());
return engine.<Object, D>map(source, Types.<Object>deProxy(source.getClass()), destination,
    TypeToken.<D>of(destinationType), typeMapName);
}

ライブラリなので知る必要は無い箇所だが、とても難しそうなジェネリクスを使った処理になっている

modelmapper/ModelMapper.java at 1e2fdc058ff930788a351f1fc2789fb33cc9face · modelmapper/modelmapper · GitHub

呼び出し元

public <D> D map(Object source, Class<D> destinationType) {
    Assert.notNull(source, "source");
    Assert.notNull(destinationType, "destinationType");
    return mapInternal(source, null, destinationType, null);
}

利用例

Game game = new Game("スプラ3");
ModelMapper modelmapper = new ModelMapper();
GameDto gameDto = modelmapper.map(game,GameDto.class);

この場合はDというのは何なのか?を特定するのが理解の第一歩です

例えば、少しマジカルな印象だがClassクラスの定義はClass<T>なので何らかの型を指定できる

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement,
                              TypeDescriptor.OfField<Class<?>>,
                              Constable {

なのでClass<D>のDを返すというのは、D型の事を指している

ものすごく単純なClass<D>のD型のオブジェクトを返す処理を書きました

private <D> D get(Class<D> clazz) throws Exception {
    // コンストラクタの取得
    Constructor<D> constructor = clazz.getConstructor();
    D obj = constructor.newInstance();
    return obj;
}

このDというのはClass<D>のDなので、Dの実際のインスタンスを返すことになる。

GameDto gameDto = null;
try {
    gameDto = get(GameDto.class);
    gameDto.name = "リフレクションで取得したインスタンス";
} catch (Exception exception) {
    exception.printStackTrace();
}
assertEquals("リフレクションで取得したインスタンス",gameDto.name);

Classクラスを返すのでなく、その型のオブジェクトを返すというのが重要なポイントです

例えば次はわかりやすいです

private <T> T get(List<T> list){
        return list.get(0);
}

このメソッドが返すのはListでなくて、Listに紐づくT型のインスタンスです

List<GameDto> list = List.of(new GameDto());
GameDto dto = this.get(list);
dto.name = "スプラ";
assertEquals("スプラ",dto.name);

本題に戻りまして...

ModelMapperの以下の処理は、Class<D>、つまりD型のインスタンスを返すことになります

public <D> D map(Object source, Class<D> destinationType) {
    Assert.notNull(source, "source");
    Assert.notNull(destinationType, "destinationType");
    return mapInternal(source, null, destinationType, null);
}

その中で一番最初に掲載したコードに行き着くわけですが、引数のdestinationはnullが渡っています。 (今回の場合)

private <D> D mapInternal(Object source, D destination, Type destinationType, String typeMapName) {
if (destination != null)
  destinationType = Types.<D>deProxy(destination.getClass());
return engine.<Object, D>map(source, Types.<Object>deProxy(source.getClass()), destination,
    TypeToken.<D>of(destinationType), typeMapName);
}

なので注目すべきはreturn節です。
"" ここでTypeToken.<D>of(destinationType)というコードでmapメソッドの引数に渡しています。destinationTypeはクラスオブジェクトです。
このofは引数のType情報に基づいてTypeTokeという型のインスタンスを返します。TypeTokeは、rawType変数としてClassオブジェクトを管理しています。→ここにdestinationTypeが入ります。 ""

""で囲った内容は少し複雑です。重要な考えはmapInternalメソッドで受け取るD destinationがnullなのに、engine.<Object, D>mapとして返却できるということです。

この動きを簡単に確認してみます。

public class MyTypeToken<T> {

    private Class<T> clazz;

    public static <T> MyTypeToken<T> of(Type type) throws Exception {
        return new MyTypeToken<T>(type);
    }

    private MyTypeToken(Type t) throws Exception{
        // キャストしてあげる
        Class<T> clazz = (Class<T>) t;
        this.clazz = clazz;
    }

    public Class<T> getClazz(){
        return this.clazz;
    }
}

このクラスは受け取ったTをClass<T>として保持します そのためにTypeをキャストしています。 →未検査警告が出ると思います。

今作ったMyTypeTokenを利用してジェネリクスで指定されている型を受け取らずに結果を返せるメソッドを定義します

private <D> D getD(D clazz, Type type){
    // clazzがnullでも、戻り値を特定できる
    D result = null;
    try {
        MyTypeToken token = MyTypeToken.<D>of(type);
        Class<D> d = token.getClazz();
        Constructor con = d.getConstructor();
        result = (D) con.newInstance();

    } catch (Exception e){
    }
    return result;
}

このメソッドは<D>としてMyTypeTokenのオブジェクトを生成します。 そしてDのインスタンスはMyTypeTokenに格納されているClass<T> clazzから生成します

このメソッドをDの指定をせずに呼び出します

GameDto g = this.getD(null,GameDto.class);
g.name = "wow";
System.out.println(g.name);
assertEquals("wow",g.name);

このテストは成功します。つまりGameDtoのインスタンスがgetDメソッドを介して取得できています。 これは奇妙でイマジナリーなジェネリクスの動きを検証しました。

ここまでくると以下の処理も追いつけると思います

return engine.<Object, D>map(source, Types.<Object>deProxy(source.getClass()), destination,
    TypeToken.<D>of(destinationType), typeMapName);