Java8の型推論でハマりまくった話

個人の日記レベルですんません、現象のみ。
書いてるコードに関しては型指定してあげれば解決したのだけど、再現させるのに苦労した。


まず、インタフェースを2つ用意。

interface A{}
interface B{}


それから、その両方を実装したクラスをふたつ用意

static class C implements A,B{}
static class D implements A,B{}


片方だけ実装したクラスもふたつ用意

static class E implements A{}
static class F implements A{}


そんで、同じ型を継承してGでくるんだものを複数とって、共通の基底クラスをGでくるんだものとして返すメソッドを定義。
※この文章では、実装したインタフェースも基底クラスと表現します。

static <T> G<T> o(G<? extends T>... t){
    return null;
}


このメソッドoに、CをくるんだGと、DをくるんだGを渡す。CとDの共通の基底クラスはAとBなので、G<A>とG<B>として受け取ることができる。これはOK

G<A>                 o01 =              o(new G<C>(), new G<D>());//OK
G<B>                 o01 =              o(new G<C>(), new G<D>());//OK


それから、Genericなクラスを用意して、同じ型を返すメソッドと同じ型のリストをくるむメソッドを定義。今回は型だけの問題なので、戻り値はnull。

static class G<U>{
    G<U> m(){
        return null;
    }
    G<List<U>> n(){
        return null;
    }
}


ここで、メソッドoの戻り値にメソッドmの呼び出しを追加するとコンパイルエラー

G<A>                 o02 =              o(new G<C>(), new G<D>()).m();//NG
G<B>                 o03 =              o(new G<C>(), new G<D>()).m();//NG


戻り値を使わなければOK。このあとに出る他のNGのやつも、戻り値使わなければだいたいOKです。

                                        o(new G<C>(), new G<D>()).m();//NG

変数の型をAを継承したものと指定するか、メソッドoの呼び出しでgeneric型を指定してあげればOK。generic型を指定すると大丈夫ということから、型推論が失敗してることがわかる。

G<? extends A>       o04 =              o(new G<C>(), new G<D>()).m();//OK
G<A>                 o05 = SqlParser.<A>o(new G<C>(), new G<D>()).m();//OK


さっき失敗したものを、もう一度メソッドoに渡してやるとOK

G<A>                 o06 =            o(o(new G<C>(), new G<D>()).m());//OK


共通の基底クラスがAだけのEとFならOK

G<A>                 o07 =              o(new G<E>(), new G<F>()).m();//OK
G<A>                 o08 =            o(o(new G<E>(), new G<F>()).m());//OK


メソッドoの引数にCとDに加えてEを付け足してもOK

G<A>                 o09 =  o(new G<C>(), new G<D>(), new G<E>()).m();//OK


メソッドoのあとに付け加えるのをメソッドnにして、Listを返すようにするとやはりNG。変数の型の範囲をひろげてもだめ。

G<List<A>>           o11 =                o(new G<C>(), new G<D>()).n();//NG
G<List<? extends A>> o12 =                o(new G<C>(), new G<D>()).n();//NG


メソッドoにちゃんと型を指定してあげると大丈夫

G<List<A>>           o13 =   SqlParser.<A>o(new G<C>(), new G<D>()).n();//OK


もう一回メソッドoに渡す作戦も、今回はNG

G<List<A>>           o14 =              o(o(new G<C>(), new G<D>()).n());//NG

内側のメソッドoにちゃんと型を指定してあげると大丈夫

G<List<A>>           o15 = o(SqlParser.<A>o(new G<C>(), new G<D>()).n());//OK


外側のメソッドoに型を指定しても内側でNG。これは戻り値使わなくてもNG

G<List<A>>           o16 = SqlParser.<A>o(o(new G<C>(), new G<D>()).n());//NG


共通の基底クラスがAだけのEとFなら大丈夫。

G<List<A>>           o17 =                o(new G<E>(), new G<F>()).n();//OK
G<List<A>>           o18 =              o(o(new G<E>(), new G<F>()).n());//OK


ということで、これひっかかるとすげーハマります。
こういうのはどこまで仕様なんだろう?