Vários curingas em um método genérico fazem o compilador Java (e eu!) Ficar muito confuso

Vamos primeiro considerar um cenário simples ( veja a fonte completa em ideone.com ):

import java.util.*; public class TwoListsOfUnknowns { static void doNothing(List list1, List list2) { } public static void main(String[] args) { List list1 = null; List list2 = null; doNothing(list1, list2); // compiles fine! } } 

Os dois curingas não estão relacionados, e é por isso que você pode chamar doNothing com uma List e uma List . Em outras palavras, os dois ? pode se referir a tipos totalmente diferentes. Daí o seguinte não compilar, o que é de se esperar ( também no 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) // in the type List is not applicable for // the arguments (List) } } 

Até aí tudo bem, mas aqui é onde as coisas começam a ficar muito confusas ( como visto em ideone.com ):

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

O código acima compila para mim no Eclipse e no sun-jdk-1.6.0.17 em ideone.com, mas deveria? Não é possível que tenhamos uma List<List> lol e uma List list , as duas situações de curingas não relacionadas análogas de TwoListsOfUnknowns ?

Na verdade, a seguinte pequena modificação em direção a essa direção não compila, o que é de se esperar ( como visto em ideone.com ):

 import java.util.*; public class LOLUnknowns2 { static void rightfullyIllegal( List<List> lol, List list) { lol.add(list); // DOES NOT COMPILE! As expected!!! // The method add(List) in the type // List<List> is not applicable for // the arguments (List) } } 

Então parece que o compilador está fazendo o seu trabalho, mas então nós pegamos isso ( como visto no ideone.com ):

 import java.util.*; public class LOLUnknowns3 { static void probablyIllegalAgain( List<List> lol, List list) { lol.add(list); // compiles fine!!! how come??? } } 

Novamente, podemos ter, por exemplo, uma List<List> lol e uma List list , então isso não deve ser compilado, certo?

Na verdade, vamos voltar ao LOLUnknowns1 mais simples (dois curingas ilimitados) e tentar ver se podemos, de fato, invocar de maneira alguma o valor de LOLUnknowns1 . Vamos tentar o caso “fácil” primeiro e escolher o mesmo tipo para os dois curingas ( como visto em 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> lol = null; List 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>, List) } } 

Isso não faz sentido! Aqui não estamos nem tentando usar dois tipos diferentes e não compila! Tornando-se uma List<List> lol e List list também fornece um erro de compilation similar! De fato, a partir de minha experimentação, a única maneira que o código compila é se o primeiro argumento é um tipo null explícito ( como visto em 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 list = null; probablyIllegal(null, list); // compiles fine! // throws NullPointerException at run-time } } 

Então as perguntas são, com relação a LOLUnknowns1 , LOLUnknowns1a e LOLUnknowns1b :

  • Que tipos de argumentos probablyIllegal aceitam ilegalmente?
  • Deve lol.add(list); compilar em tudo? É typescript?
  • Isso é um erro do compilador ou estou entendendo mal as regras de conversão de captura para curingas?

Apêndice A: Double LOL?

Caso alguém esteja curioso, isso compila bem ( como visto em 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); } } 

Apêndice B: Curingas nesteds – o que eles realmente querem dizer ???

Investigações posteriores indicam que talvez vários curingas não tenham nada a ver com o problema, mas sim um curinga nested é a fonte da confusão.

 import java.util.*; public class IntoTheWild { public static void main(String[] args) { List list = new ArrayList(); // compiles fine! List<List> lol = new ArrayList<List>(); // DOES NOT COMPILE!!! // Type mismatch: cannot convert from // ArrayList<List> to List<List> } } 

Portanto, talvez pareça que uma List<List> não é uma List<List> . De fato, enquanto qualquer List é uma List , Não parece que qualquer List<List> seja uma List<List> ( como visto em ideone.com ):

 import java.util.*; public class IntoTheWild2 { static  List makeItWild(List list) { return list; // compiles fine! } static  List<List> makeItWildLOL(List<List> lol) { return lol; // DOES NOT COMPILE!!! // Type mismatch: cannot convert from // List<List> to List<List> } } 

Uma nova questão surge, então: o que é uma List<List> ?

Como o Apêndice B indica, isso não tem nada a ver com vários caracteres curinga, mas sim entender mal o que a List> Realmente significa.

Vamos primeiro lembrar o que significa que os genéricos Java são invariantes:

  1. Um Integer é um Number
  2. Uma List não é uma List
  3. Uma List é uma List< ? extends Number> List< ? extends Number>

Agora, simplesmente aplicamos o mesmo argumento à nossa situação de lista aninhada (veja o apêndice para mais detalhes) :

  1. Uma List é (capturável por) uma List< ?>
  2. Uma List> NÃO é (capturável por) uma List>
  3. Uma List> É (capturável por) uma List< ? extends List> List< ? extends List>

Com esse entendimento, todos os trechos da pergunta podem ser explicados. A confusão surge em (falsamente) acreditar que um tipo como List> pode capturar tipos como List> , List> , etc. Isto NÃO é verdade.

Isto é, uma List> :

  • NÃO é uma lista cujos elementos são listas de algum tipo desconhecido.
    • … isso seria uma List< ? extends List> List< ? extends List>
  • Em vez disso, é uma lista cujos elementos são listas de qualquer tipo.

Trechos

Aqui está um trecho para ilustrar os pontos acima:

 List> lolAny = new ArrayList>(); lolAny.add(new ArrayList()); lolAny.add(new ArrayList()); // lolAny = new ArrayList>(); // DOES NOT COMPILE!! List< ? extends List> lolSome; lolSome = new ArrayList>(); lolSome = new ArrayList>(); 

Mais trechos

Aqui está outro exemplo com o caractere curinga nested:

 List> lolAnyNum = new ArrayList>(); lolAnyNum.add(new ArrayList()); lolAnyNum.add(new ArrayList()); // lolAnyNum.add(new ArrayList()); // DOES NOT COMPILE!! // lolAnyNum = new ArrayList>(); // DOES NOT COMPILE!! List< ? extends List> lolSomeNum; lolSomeNum = new ArrayList>(); lolSomeNum = new ArrayList>(); // lolSomeNum = new ArrayList>(); // DOES NOT COMPILE!! 

Voltar para a pergunta

Para voltar aos trechos da pergunta, o seguinte se comporta como esperado ( como visto em 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> is not // applicable for the arguments (List) } public static void main(String[] args) { List list = null; List> lolString = null; List> lolInteger = null; // these casts are valid nowDefinitelyIllegal(lolString, list); nowDefinitelyIllegal(lolInteger, list); } } 

lol.add(list); é ilegal porque podemos ter uma List> lol e uma Listlist . Na verdade, se comentarmos a declaração ofensiva, o código compila e é exatamente isso que temos com a primeira invocação na main .

Todos os methods probablyIllegal da questão não são ilegais. Eles são todos perfeitamente legais e seguros. Não há absolutamente nenhum bug no compilador. Está fazendo exatamente o que é suposto fazer.


Referências

  • Perguntas frequentes sobre genéricos Java de Angelika Langer
    • Quais relacionamentos de super-subtipos existem entre instanciações de tipos genéricos?
    • Posso criar um object cujo tipo é um tipo com parâmetros curinga?
  • JLS 5.1.10 Captura de Conversão

Perguntas relacionadas

  • Qualquer maneira simples de explicar porque eu não posso fazer List animals = new ArrayList() ?
  • Genérico genérico nested em Java não compilará

Apêndice: as regras de conversão de captura

(Isso foi levantado na primeira revisão da resposta; é um complemento valioso para o argumento invariante de tipo.)

5.1.10 Conversão de Captura

Deixe G nomear uma declaração de tipo genérico com n parâmetros de tipo formal A 1 … A n com os limites correspondentes U 1 … U n . Existe uma conversão de captura de G 1 … T n > para G 1 … S n > , onde, para 1 < = i <= n :

  1. Se T i é um argumento de tipo curinga do formulário ? então …
  2. Se T i é um argumento de tipo curinga do formulário ? extends ? extends B i , então …
  3. Se T i é um argumento de tipo curinga do formulário ? super ? super B i , então …
  4. Caso contrário, S i = T i .

A conversão de captura não é aplicada recursivamente.

Esta seção pode ser confusa, especialmente no que diz respeito à aplicação não recursiva da conversão de captura (aqui CC ), mas a chave é que nem todos ? pode CC; depende de onde aparece . Não há aplicação recursiva na regra 4, mas quando as regras 2 ou 3 se aplicam, a respectiva B i pode ser ela mesma o resultado de um CC.

Vamos trabalhar com alguns exemplos simples:

  • List< ?> Pode List CC List
    • O ? pode CC pela regra 1
  • List< ? extends Number> List< ? extends Number> pode CC List
    • O ? pode CC pela regra 2
    • Ao aplicar a regra 2, B i é simplesmente Number
  • List< ? extends Number> List< ? extends Number> não pode CC List
    • O ? pode CC pela regra 2, mas erro de tempo de compilation ocorre devido a tipos incompatíveis

Agora vamos tentar um pouco de aninhamento:

  • List> NÃO pode List> CC List>
    • Regra 4 se aplica, e CC não é recursiva, então o ? não pode CC
  • List< ? extends List> List< ? extends List> pode List> CC List>
    • O primeiro ? pode CC pela regra 2
    • Ao aplicar a regra 2, B i é agora uma List< ?> , Que pode List CC List
    • Ambos ? pode CC
  • List< ? extends List> List< ? extends List> pode CC List>
    • O primeiro ? pode CC pela regra 2
    • Ao aplicar a regra 2, B i é agora uma List< ? extends Number> List< ? extends Number> , que pode CC List
    • Ambos ? pode CC
  • List< ? extends List> List< ? extends List> NÃO pode List> CC List>
    • O primeiro ? pode CC pela regra 2
    • Ao aplicar a regra 2, B i é agora uma List< ? extends Number> List< ? extends Number> , que pode CC, mas fornece um erro de tempo de compilation quando aplicado a List
    • Ambos ? pode CC

Para ilustrar melhor por que alguns ? CC e outros não podem, considere a seguinte regra: você não pode instanciar diretamente um tipo curinga. Ou seja, o seguinte dá um erro de tempo de compilation:

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

No entanto, o seguinte compila muito bem:

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

A razão WildSnippet2 qual o WildSnippet2 compila é porque, como explicado acima, nenhum dos ? pode CC. No WildSnippet1 , o K ou o V (ou ambos) do HashMap podem CC, o que torna a instanciação direta através de new ilegal.

  • Nenhum argumento com genéricos deve ser aceito. No caso de LOLUnknowns1b o null é aceito como se o primeiro argumento fosse typescript como List . Por exemplo, isso compila:

     List lol = null; List list = null; probablyIllegal(lol, list); 
  • IMHO lol.add(list); nem deveria compilar mas como lol.add() precisa de um argumento do tipo List< ?> e como a lista se encheckbox em List< ?> ele funciona.
    Um exemplo estranho que me faz pensar nessa teoria é:

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

    lol.add() precisa de um argumento do tipo List< ? extends Number> List< ? extends Number> e lista é digitada como List< ? extends Integer> List< ? extends Integer> , ele se encheckbox. Não funcionará se não corresponder. A mesma coisa para o LOL duplo e outros curingas nesteds, desde que a primeira captura corresponda à segunda, tudo está bem (e não poderia ser).

  • Mais uma vez, não tenho certeza, mas realmente parece um bug.

  • Fico feliz em não ser o único a usar variables lol o tempo todo.

Recursos :
http://www.angelikalanger.com , uma FAQ sobre genéricos

EDITOS:

  1. Adicionado comentário sobre o Double Lol
  2. E curingas nesteds.

não é um especialista, mas acho que consigo entender.

vamos mudar o seu exemplo para algo equivalente, mas com mais tipos distintivos:

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

vamos alterar Listar para [] para ser mais esclarecedor:

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

Agora, x não é um array de algum tipo de class. é uma matriz de qualquer tipo de class. ele pode conter uma Class e uma Class . isso não pode ser expresso com o parâmetro de tipo comum:

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

Class< ?> É um tipo super de Class para qualquer T Se pensarmos que um tipo é um conjunto de objects , defina Class< ?> a união de todos os conjuntos de Class para todos os T (inclui-se? Eu não sei …)