Como os genéricos dos genéricos funcionam?

Embora eu entenda alguns dos casos-chave de genéricos, estou perdendo alguma coisa com o exemplo a seguir.

Eu tenho a seguinte class

1 public class Test { 2 public static void main(String[] args) { 3 Test t = new Test(); 4 List<Test> l =Collections.singletonList(t); 5 } 6 } 

A linha 4 me dá o erro

 Type mismatch: cannot convert from List<Test> to List<Test>`. 

Obviamente, o compilador acha que o diferente ? não são realmente iguais. Enquanto meu sentimento me diz, isso está correto.

Alguém pode fornecer um exemplo de como obter um erro de tempo de execução se a linha 4 fosse legal?

EDITAR:

Para evitar confusão, substituí o =null na Linha 3 por uma atribuição concreta

Como Kenny observou em seu comentário, você pode contornar isso com:

 List> l = Collections.>singletonList(t); 

Isso imediatamente nos diz que a operação não é insegura , é apenas uma vítima de inferência limitada. Se não fosse seguro, o acima não compilaria.

Uma vez que o uso de parâmetros de tipo explícito em um método genérico como acima só é necessário para agir como uma dica , podemos supor que isso seja necessário aqui é uma limitação técnica do mecanismo de inferência. De fato, o compilador Java 8 está atualmente disponível para fornecer muitos aprimoramentos à inferência de tipos . Não tenho certeza se seu caso específico será resolvido.

Então, o que está realmente acontecendo?

Bem, o erro de compilation que estamos obtendo mostra que o parâmetro de tipo T de Collections.singletonList está sendo inferido como sendo capture> capture> . Em outras palavras, o curinga tem alguns metadados associados a ele que o vincula a um contexto específico.

  • A melhor maneira de pensar em uma captura de um caractere curinga ( capture ) é como um parâmetro de tipo sem nome dos mesmos limites (isto é, , mas sem poder fazer referência a T ).
  • A melhor maneira de “liberar” o poder da captura é vinculá-lo a um parâmetro de tipo nomeado de um método genérico. Vou demonstrar isso em um exemplo abaixo. Veja o tutorial Java “Wildcard Capture and Helper Methods” (obrigado pela referência @WChargin) para mais informações.

Digamos que queremos ter um método que desloque uma lista, envolvendo-a na parte de trás. Então vamos supor que nossa lista tenha um tipo desconhecido (curinga).

 public static void main(String... args) { List list = new ArrayList<>(Arrays.asList("a", "b", "c")); List cycledTwice = cycle(cycle(list)); } public static  List cycle(List list) { list.add(list.remove(0)); return list; } 

Isso funciona bem, porque T é resolvido para capture capture , não ? extends String ? extends String . Se, em vez disso, utilizássemos essa implementação não genérica do ciclo:

 public static List cycle(List list) { list.add(list.remove(0)); return list; } 

Ele falharia na compilation, porque não tornamos a captura acessível atribuindo-a a um parâmetro de tipo.

Então isso começa a explicar por que o consumidor de singletonList se beneficiaria do tipo inferer resolvendo T para Test Test e, portanto, retornando uma List>> List>> vez de uma List> List> .

Mas por que um não é atribuível ao outro?

Por que não podemos simplesmente atribuir uma List>> List>> para uma List> List> ?

Bem, se pensarmos no fato de que capture capture é o equivalente a um parâmetro de tipo anônimo com um limite superior de Number , então podemos transformar essa questão em “Por que a seguinte não compila?” (não!):

 public static  List> assign(List> t) { return t; } 

Isso tem um bom motivo para não compilar. Se isso acontecesse, então isso seria possível:

 //all this would be valid List> doubleTests = null; List> numberTests = assign(doubleTests); Test integerTest = null; numberTests.add(integerTest); //type error, now doubleTests contains a Test 

Então, por que ser um trabalho explícito?

Vamos voltar ao começo. Se o acima for inseguro, então como isso é permitido:

 List> l = Collections.>singletonList(t); 

Para que isso funcione, isso implica que o seguinte é permitido:

 Test> capturedT; Test t = capturedT; 

Bem, isso não é uma syntax válida, pois não podemos referenciar a captura explicitamente, então vamos avaliá-la usando a mesma técnica acima! Vamos vincular a captura a uma variante diferente de “assign”:

 public static  Test assign(Test t) { return t; } 

Isso compila com sucesso. E não é difícil ver por que isso deveria ser seguro. É o mesmo caso de uso de algo como

 List l = new List(); 

Não há nenhum erro em tempo de execução potencial, é apenas fora da capacidade do compilador para determinar estaticamente isso. Sempre que você causa uma inferência de tipos, gera automaticamente uma nova captura de e duas capturas não são consideradas equivalentes.

Portanto, se você remover a inferência da invocação de singletonList especificando para ela:

 List> l = Collections.>singletonList(t); 

Funciona bem. O código gerado não é diferente do que se sua chamada tivesse sido legal, é apenas uma limitação do compilador que ele não consegue descobrir isso sozinho.

A regra que uma inferência cria uma captura e captura não é compatível é o que impede que este exemplo de tutorial seja compilado e explodido em tempo de execução:

 public static void swap(List l1, List l2) { Number num = l1.get(0); l1.add(0, l2.get(0)); l2.add(0, num); } 

Sim, a especificação da linguagem e o compilador provavelmente poderiam ser mais sofisticados para diferenciar seu exemplo, mas não é e é simples o suficiente para contornar.

Talvez isso possa explicar o problema do compilador:

 List myNums = new ArrayList(); 

Essa lista de curingas genéricas pode conter todos os elementos que se estendem de Number. Então está OK para atribuir uma lista Integer a ele. No entanto, agora eu poderia adicionar um Double para myNums porque Double também está se estendendo de Number, o que levaria a um problema de tempo de execução. Então o compilador proíbe todo access de escrita ao myNums e eu só posso usar methods de leitura nele, porque eu só sei o que eu recebo pode ser convertido em Number.

E assim, o compilador está reclamando sobre muitas coisas que você pode fazer com um genérico desse tipo. Às vezes ele é louco por coisas que você pode garantir que eles estão seguros e OK.

Mas, felizmente, há um truque para contornar este erro, assim você pode testar o que pode, talvez, quebrar isso:

 public static void main(String[] args) { List list1 = new ArrayList(); List> list2 = copyHelper(list1); } private static  List> copyHelper(List list) { return Collections.singletonList(list); } 

A razão é que o compilador não sabe que seus tipos curinga são do mesmo tipo.

Também não sabe que sua instância é null . Embora null seja um membro de todos os tipos, o compilador considera apenas os tipos declarados , não o que o valor da variável pode conter, quando a verificação de tipo.

Se o código for executado, não causará uma exceção, mas isso é apenas porque o valor é nulo. Ainda existe uma possível incompatibilidade de tipos, e é isso que o trabalho do compilador é – não permitir incompatibilidades de tipos.

Dê uma olhada no tipo de apagamento . O problema é que “tempo de compilation” é a única oportunidade que o Java tem para impor esses genéricos, então se ele deixar isso passar, não será capaz de dizer se você tentou inserir algo inválido. Isto é realmente uma coisa boa, porque significa que uma vez que o programa compila, então Generics não incorre em qualquer penalidade de performance em tempo de execução.

Vamos tentar ver seu exemplo de outra maneira (vamos usar dois tipos que estendem Number mas se comportam de maneira muito diferente). Considere o seguinte programa:

 import java.math.BigDecimal; import java.util.*; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799<>(); t.val = new BigDecimal(Math.PI); List> l = Collections.singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } } 

Esta saída (como seria de esperar):

 3.141592653589793115997963468544185161590576171875 

Agora, assumindo que o código que você apresentou não causou um erro no compilador:

 import java.math.BigDecimal; import java.util.concurrent.atomic.AtomicLong; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799<>(); t.val = new BigDecimal(Math.PI); List> l = Collections.singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } } 

O que você esperaria que a saída fosse? Você não pode razoavelmente lançar um BigDecimal em um AtomicLong (você pode construir um AtomicLong a partir do valor de um BigDecimal, mas o casting e a construção são coisas diferentes e os Generics são implementados como tempo de compilation para garantir que os lançamentos sejam bem-sucedidos). Quanto ao comentário do @ KennyTM, um tipo concreto está procurando no exemplo inicial, mas tente compilar isso:

 import java.math.BigDecimal; import java.util.*; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799(); t.val = new BigDecimal(Math.PI); List> l = Collections.>singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } } 

Isso causará um erro no momento em que você tentar definir um valor para t.val .