web-dev-qa-db-ja.com

ジェネリックメソッドに複数のワイルドカードがあると、Javaコンパイラ(および私!)が非常に混乱します

最初に簡単なシナリオを考えてみましょう( ideone.comの完全なソースを参照 ):

_import Java.util.*;

public class TwoListsOfUnknowns {
    static void doNothing(List<?> list1, List<?> list2) { }

    public static void main(String[] args) {
        List<String> list1 = null;
        List<Integer> list2 = null;
        doNothing(list1, list2); // compiles fine!
    }
}
_

2つのワイルドカードは無関係です。そのため、doNothingを_List<String>_および_List<Integer>_で呼び出すことができます。言い換えると、2つの_?_は完全に異なるタイプを参照できます。したがって、以下はコンパイルされません。これは予想されることです( ideone.comでも ):

_import Java.util.*;

public class TwoListsOfUnknowns2 {
    static void doSomethingIllegal(List<?> list1, List<?> list2) {
        list1.addAll(list2); // DOES NOT COMPILE!!!
            // The method addAll(Collection<? extends capture#1-of ?>)
            // in the type List<capture#1-of ?> is not applicable for
            // the arguments (List<capture#2-of ?>)
    }
}
_

これまでのところ良いですが、ここで物事が非常に混乱し始めます( ideone.comで見られるように ):

_import Java.util.*;

public class LOLUnknowns1 {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }
}
_

上記のコードは、Eclipseおよびideone.comの_Sun-jdk-1.6.0.17_でコンパイルされますが、コンパイルする必要がありますか? _List<List<Integer>> lol_と_List<String> list_、TwoListsOfUnknownsからの類似した2つの無関係なワイルドカード状況がある可能性はありませんか?

実際、その方向に向けた次のわずかな変更はコンパイルされません。これは予想されることです( ideone.comで見られるように ):

_import Java.util.*;

public class LOLUnknowns2 {
    static void rightfullyIllegal(
            List<List<? extends Number>> lol, List<?> list) {

        lol.add(list); // DOES NOT COMPILE! As expected!!!
            // The method add(List<? extends Number>) in the type
            // List<List<? extends Number>> is not applicable for
            // the arguments (List<capture#1-of ?>)
    }
}
_

したがって、コンパイラがその仕事をしているように見えますが、次のようになります( ideone.comで見られるように ):

_import Java.util.*;

public class LOLUnknowns3 {
    static void probablyIllegalAgain(
            List<List<? extends Number>> lol, List<? extends Number> list) {

        lol.add(list); // compiles fine!!! how come???
    }
}
_

繰り返しますが、例えば_List<List<Integer>> lol_と_List<Float> list_なので、コンパイルしないでくださいね。

実際、より単純な_LOLUnknowns1_(2つの無制限のワイルドカード)に戻って、実際にprobablyIllegalを何らかの方法で呼び出すことができるかどうかを確認してみましょう。最初に「簡単な」ケースを試して、2つのワイルドカードに同じタイプを選択しましょう( ideone.comで見られるように ):

_import Java.util.*;

public class LOLUnknowns1a {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }

    public static void main(String[] args) {
        List<List<String>> lol = null;
        List<String> list = null;
        probablyIllegal(lol, list); // DOES NOT COMPILE!!
            // The method probablyIllegal(List<List<?>>, List<?>)
            // in the type LOLUnknowns1a is not applicable for the
            // arguments (List<List<String>>, List<String>)
    }
}
_

これは意味がありません!ここでは、2つの異なるタイプを使用しようとさえしておらず、コンパイルもされていません。 _List<List<Integer>> lol_および_List<String> list_にすると、同様のコンパイルエラーが発生します。実際、私の実験から、コードをコンパイルする唯一の方法は、最初の引数が明示的なnull型である場合です( ideone.comで見られるように ):

_import Java.util.*;

public class LOLUnknowns1b {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }

    public static void main(String[] args) {
        List<String> list = null;
        probablyIllegal(null, list); // compiles fine!
            // throws NullPointerException at run-time
    }
}
_

したがって、質問は、_LOLUnknowns1_、_LOLUnknowns1a_、および_LOLUnknowns1b_に関して次のようになります。

  • probablyIllegalはどのような種類の引数を受け入れますか?
  • lol.add(list);をコンパイルする必要がありますか?タイプセーフですか?
  • これはコンパイラのバグですか、それともワイルドカードのキャプチャ変換ルールを誤解していますか?

付録A:ダブルLOL?

誰かが興味を持っている場合、これはうまくコンパイルされます( ideone.comで見られるように ):

_import Java.util.*;

public class DoubleLOL {
    static void omg2xLOL(List<List<?>> lol1, List<List<?>> lol2) {
        // compiles just fine!!!
        lol1.addAll(lol2);
        lol2.addAll(lol1);
    }
}
_

付録B:ネストされたワイルドカード-実際にはどういう意味ですか?

さらなる調査は、おそらく複数のワイルドカードが問題とは何の関係もないことを示していますが、むしろネストされたワイルドカードが混乱の原因です。

_import Java.util.*;

public class IntoTheWild {

    public static void main(String[] args) {
        List<?> list = new ArrayList<String>(); // compiles fine!

        List<List<?>> lol = new ArrayList<List<String>>(); // DOES NOT COMPILE!!!
            // Type mismatch: cannot convert from
            // ArrayList<List<String>> to List<List<?>>
    }
}
_

したがって、おそらく_List<List<String>>_は_List<List<?>>_ではないように見えます。実際、_List<E>_は_List<?>_ですが、_List<List<E>>_は_List<List<?>>_のようには見えません( ideone.comで見られるように =):

_import Java.util.*;

public class IntoTheWild2 {
    static <E> List<?> makeItWild(List<E> list) {
        return list; // compiles fine!
    }
    static <E> List<List<?>> makeItWildLOL(List<List<E>> lol) {
        return lol;  // DOES NOT COMPILE!!!
            // Type mismatch: cannot convert from
            // List<List<E>> to List<List<?>>
    }
}
_

次に、新しい質問が発生します。_List<List<?>>_とは何ですか?

59

付録Bが示すように、これは複数のワイルドカードとは関係ありませんが、_List<List<?>>_が実際に何を意味するのかを誤解しています。

まず、Javaジェネリックスが不変であることの意味を思い出してみましょう:

  1. IntegerNumberです
  2. _List<Integer>_は[〜#〜] not [〜#〜] a _List<Number>_
  3. _List<Integer>_ [〜#〜] is [〜#〜] a _List<? extends Number>_

ネストされたリストの状況に同じ引数を適用するだけです。 (詳細については、付録を参照してください)

  1. _List<String>_は_List<?>_です(キャプチャ可能)
  2. _List<List<String>>_は[〜#〜] not [〜#〜](キャプチャ可能)_List<List<?>>_
  3. _List<List<String>>_ [〜#〜] is [〜#〜](キャプチャ可能)_List<? extends List<?>>_

この理解により、質問のすべてのスニペットを説明できます。混乱は、_List<List<?>>_のようなタイプが_List<List<String>>_、_List<List<Integer>>_などのタイプをキャプチャできると(誤って)信じることで発生します。これは[〜#〜] not [〜# 〜] true。

つまり、_List<List<?>>_:

  • is [〜#〜] not [〜#〜]要素が1つの未知のタイプのリストであるリスト。
    • ...それは_List<? extends List<?>>_になります
  • 代わりに、要素が[〜#〜] any [〜#〜]タイプのリストであるリストです。

切れ端

上記のポイントを説明するためのスニペットを次に示します。

_List<List<?>> lolAny = new ArrayList<List<?>>();

lolAny.add(new ArrayList<Integer>());
lolAny.add(new ArrayList<String>());

// lolAny = new ArrayList<List<String>>(); // DOES NOT COMPILE!!

List<? extends List<?>> lolSome;

lolSome = new ArrayList<List<String>>();
lolSome = new ArrayList<List<Integer>>();
_

その他のスニペット

ネストされたワイルドカードが制限されている別の例を次に示します。

_List<List<? extends Number>> lolAnyNum = new ArrayList<List<? extends Number>>();

lolAnyNum.add(new ArrayList<Integer>());
lolAnyNum.add(new ArrayList<Float>());
// lolAnyNum.add(new ArrayList<String>());     // DOES NOT COMPILE!!

// lolAnyNum = new ArrayList<List<Integer>>(); // DOES NOT COMPILE!!

List<? extends List<? extends Number>> lolSomeNum;

lolSomeNum = new ArrayList<List<Integer>>();
lolSomeNum = new ArrayList<List<Float>>();
// lolSomeNum = new ArrayList<List<String>>(); // DOES NOT COMPILE!!
_

質問に戻る

質問のスニペットに戻ると、次のように動作します( ideone.comで見られるように ):

_public class LOLUnknowns1d {
    static void nowDefinitelyIllegal(List<? extends List<?>> lol, List<?> list) {
        lol.add(list); // DOES NOT COMPILE!!!
            // The method add(capture#1-of ? extends List<?>) in the
            // type List<capture#1-of ? extends List<?>> is not 
            // applicable for the arguments (List<capture#3-of ?>)
    }
    public static void main(String[] args) {
        List<Object> list = null;
        List<List<String>> lolString = null;
        List<List<Integer>> lolInteger = null;

        // these casts are valid
        nowDefinitelyIllegal(lolString, list);
        nowDefinitelyIllegal(lolInteger, list);
    }
}
_

lol.add(list);は、_List<List<String>> lol_と_List<Object> list_がある可能性があるため、違法です。実際、問題のあるステートメントをコメントアウトすると、コードがコンパイルされ、それがmainの最初の呼び出しで得られるものとまったく同じです。

問題のprobablyIllegalメソッドはすべて違法ではありません。それらはすべて完全に合法でタイプセーフです。コンパイラにバグは一切ありません。それはまさにそれがすることになっていることをしている。


参考文献

関連する質問


付録:キャプチャ変換のルール

(これは回答の最初の改訂で取り上げられました。これは、型不変の引数を補足する価値があります。)

5.1.10キャプチャ変換

[〜#〜] g [〜#〜]ジェネリック型宣言にn正式な型パラメーターA1…an対応する境界を持つ1…UnG <Tからのキャプチャ変換が存在します1…tn>からG <S1…sn>、ここで、1 <= i <= n

  1. もしTは、_?_の形式のワイルドカード型引数です。その後…
  2. もしTは、_? extends_ Bの形式のワイルドカード型引数です。、次に…
  3. もしTは、_? super_ Bの形式のワイルドカード型引数です。、次に…
  4. それ以外の場合S = T

キャプチャ変換は再帰的に適用されません。

このセクションは、特にキャプチャ変換の非再帰的アプリケーション(ここでは[〜#〜] cc [〜#〜])に関して混乱する可能性がありますが、重要なのはすべての_?_がCCできるわけではありません。表示される場所によって異なります。ルール4には再帰的な適用はありませんが、ルール2または3が適用される場合、それぞれのBそれ自体がCCの結果である可能性があります。

いくつかの簡単な例を見てみましょう。

  • _List<?>_はCC _List<String>_
    • _?_はルール1でCCできます
  • _List<? extends Number>_はCC _List<Integer>_
    • _?_はルール2でCCできます
    • ルール2を適用する場合Bは単にNumberです
  • _List<? extends Number>_ can [〜#〜] not [〜#〜] CC _List<String>_
    • _?_はルール2でCCできますが、互換性のないタイプが原因でコンパイル時エラーが発生します

それでは、ネストを試してみましょう。

  • _List<List<?>>_ can [〜#〜] not [〜#〜] CC _List<List<String>>_
    • ルール4が適用され、CCは再帰的ではないため、_?_は[〜#〜] not [〜#〜] CCを実行できます。
  • _List<? extends List<?>>_はCC _List<List<String>>_
    • 最初の_?_はルール2でCCできます
    • ルール2を適用する場合Bは_List<?>_になり、CC _List<String>_になります。
    • 両方の_?_はCCできます
  • _List<? extends List<? extends Number>>_はCC _List<List<Integer>>_
    • 最初の_?_はルール2でCCできます
    • ルール2を適用する場合Bは_List<? extends Number>_になり、CC _List<Integer>_になります。
    • 両方の_?_はCCできます
  • _List<? extends List<? extends Number>>_ can [〜#〜] not [〜#〜] CC _List<List<Integer>>_
    • 最初の_?_はルール2でCCできます
    • ルール2を適用する場合Bは_List<? extends Number>_になり、CCに変換できますが、_List<Integer>_に適用するとコンパイル時エラーが発生します
    • 両方の_?_はCCできます

一部の_?_がCCでき、他の__ができない理由をさらに説明するために、次のルールを検討してください。ワイルドカードタイプを直接インスタンス化できる[〜#〜] not [〜#〜]。つまり、次の場合、コンパイル時エラーが発生します。

_    // WildSnippet1
    new HashMap<?,?>();         // DOES NOT COMPILE!!!
    new HashMap<List<?>, ?>();  // DOES NOT COMPILE!!!
    new HashMap<?, Set<?>>();   // DOES NOT COMPILE!!!
_

ただし、以下は問題なくコンパイルされます。

_    // WildSnippet2
    new HashMap<List<?>,Set<?>>();            // compiles fine!
    new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!
_

_WildSnippet2_がコンパイルされる理由は、上で説明したように、_?_のいずれもCCできないためです。 _WildSnippet1_では、_HashMap<K,V>_のKまたはV(あるいはその両方)のいずれかがCCになり、newを介した直接インスタンス化が違法になります。

70
  • 引数なしジェネリックス付きは受け入れられるべきです。 _LOLUnknowns1b_の場合、最初の引数がnullとして入力されたかのようにListが受け入れられます。たとえば、これはコンパイルされます:

    _List lol = null;
    List<String> list = null;
    probablyIllegal(lol, list);
    _
  • IMHO lol.add(list);はコンパイルすらすべきではありませんが、lol.add()にはタイプ_List<?>_の引数が必要であり、リストが_List<?>_に収まるので機能します。
    この理論を思い起こさせる奇妙な例は次のとおりです。

    _static void probablyIllegalAgain(List<List<? extends Number>> lol, List<? extends Integer> list) {
        lol.add(list); // compiles fine!!! how come???
    }
    _

    lol.add()にはタイプ_List<? extends Number>_の引数が必要で、リストは_List<? extends Integer>_と入力され、適合します。一致しない場合は機能しません。ダブルLOLやその他のネストされたワイルドカードについても同じことが言えます。最初のキャプチャが2番目のキャプチャと一致する限り、すべてが問題ありません(そして魂はそうではありません)。

  • 繰り返しますが、よくわかりませんが、実際にはバグのようです。

  • lol変数を常に使用しているのは私だけではないことを嬉しく思います。

リソース:
http://www.angelikalanger.com 、a FAQジェネリック医薬品について

編集:

  1. ダブルロルについてのコメントを追加しました
  2. そしてネストされたワイルドカード。
2
Colin Hebert

専門家ではありませんが、理解できると思います。

例を同等のものに変更しましょう。ただし、より特徴的なタイプを使用します。

static void probablyIllegal(List<Class<?>> x, Class<?> y) {
    x.add(y); // this compiles!! how come???
}

リストを[]に変更して、よりわかりやすくしましょう。

static void probablyIllegal(Class<?>[] x, Class<?> y) {
    x.add(y); // this compiles!! how come???
}

現在、xはnotsomeタイプのクラスの配列です。これは、anyタイプのクラスの配列です。 Class<String>およびClass<Int>を含めることができます。これは通常のタイプパラメータでは表現できません。

static<T> void probablyIllegal(Class<T>[] x  //homogeneous! not the same!

Class<?>は、anyTClass<T>のスーパータイプです。 typeオブジェクトのセットであると考えると、setClass<?>は、すべてのTClass<T>のすべてのsetsの和集合です。 (それ自体が含まれていますか?私は知りません...)

0
irreputable