Inferência do tipo Java: a referência é ambígua no Java 8, mas não no Java 7

Vamos dizer que temos 2 aulas. Uma class vazia Base e uma subclass dessa class Derived .

 public class Base {} public class Derived extends Base {} 

Então nós temos alguns methods em outra class:

 import java.util.Collection public class Consumer { public void test() { set(new Derived(), new Consumer().get()); } public  T get() { return (T) new Derived(); } public void set(Base i, Derived b) { System.out.println("base"); } public void set(Derived d, Collection o) { System.out.println("object"); } } 

Isso compila e executa com êxito no Java 7, mas não compila no Java 8. O erro:

 Error:(8, 9) java: reference to set is ambiguous both method set(Base,Derived) in Consumer and method set(Derived,java.util.Collection) in Consumer match 

Por que funciona no Java 7, mas não no Java 8? Como poderia corresponder à coleção?

O problema é que a inferência de tipos foi melhorada . Você tem um método como

 public  T get() { return (T) new Derived(); } 

que basicamente diz, “o chamador pode decidir qual subclass de Base eu retorno”, o que é um absurdo óbvio. Todo compilador deve fornecer um aviso não verificado sobre o tipo de conversão (T) aqui.

Agora você tem uma chamada de método:

 set(new Derived(), new Consumer().get()); 

Lembre-se de que seu método Consumer.get() diz “o chamador pode decidir o que eu retorno”. Portanto, é perfeitamente correto supor que poderia haver um tipo que estenda o Base e implemente a Collection ao mesmo tempo. Então o compilador diz “Eu não sei se devo chamar set(Base i, Derived b) ou set(Derived d, Collection< ? extends Consumer> o) ”.

Você pode “consertá-lo” chamando set(new Derived(), new Consumer().get()); mas para ilustrar a loucura do seu método, note que você também pode alterá-lo para

 public > void test() { set(new Derived(), new Consumer().get()); } 

que agora chamará set(Derived d, Collection< ? extends Consumer> o) sem qualquer aviso de compilador. A operação insegura real aconteceu dentro do método get .

Portanto, a correção correta seria remover o parâmetro type do método get e declarar o que ele realmente retorna, Derived .


By the way, o que me irrita, é a sua afirmação de que este código poderia ser compilado em Java 7. Sua inferência de tipo limitado com chamadas de método aninhadas leva a tratar o método get em um contexto de invocação aninhada como retornar Base que não pode ser passado para um método esperando um Derived . Como consequência, tentar compilar esse código usando um compilador Java 7 em conformidade também falhará, mas por razões diferentes.

Bem, não é como se o Java7 estivesse feliz em executá-lo. Dá alguns avisos antes de dar o erro:

 jatin@jatin-~$ javac -Xlint:unchecked -source 1.7 com/company/Main.java warning: [options] bootstrap class path not set in conjunction with -source 1.7 com/company/Main.java:19: error: no suitable method found for set(Derived,Base) set(new Derived(), new Consumer().get()); ^ method Consumer.set(Base,Derived) is not applicable (argument mismatch; Base cannot be converted to Derived) method Consumer.set(Derived,Collection< ? extends Consumer>) is not applicable (argument mismatch; Base cannot be converted to Collection< ? extends Consumer>) com/company/Main.java:28: warning: [unchecked] unchecked cast return (T) new Derived(); ^ required: T found: Derived where T is a type-variable: T extends Base declared in method get() 

A questão é esta:

 set(new Derived(), new Consumer().get()); 

Quando fazemos new Consumer().get() , se olharmos para assinatura de get . Ele nos retorna um tipo T Sabemos que T alguma forma estende a Base . Mas nós não sabemos o que T é especificamente. Pode ser qualquer coisa. Então, se não podemos decidir especificamente o que T é, então como pode o compilador?

Uma maneira de dizer ao compilador é codificando-o e dizendo-o especificamente por: set(new Derived(), new Consumer().get()); .

A razão acima é extremamente extremamente (repetidamente intencionalmente) perigosa, é quando você tenta fazer isso:

 class NewDerived extends Base { public String getName(){return "name";}; } NewDerived d = new Consumer().get(); System.out.println(d.getName()); 

No Java7 (ou qualquer versão do Java), ele lançará uma exceção no tempo de execução:

Exceção no segmento “main” java.lang.ClassCastException: com.company.Derived não pode ser convertido em com.company.NewDerived

Porque get retorna um object do tipo Derived mas você mencionou ao compilador que é de NewDerived . E ele não pode converter Derived para NewDerived corretamente. É por isso que mostra um aviso.


De acordo com o erro, agora entendemos o que está errado com o new Consumer().get() . É tipo é something that extends base . Fazendo set(new Derived(), new Consumer().get()); procura por um método que receba argumentos como Derived (or any super class of it), something that extends Base .

Agora ambos os seus methods se qualificam para o primeiro argumento. De acordo com o segundo argumento, something that extends base , novamente ambos são elegíveis como algo que pode ser derivado ou estender derivado ou estender coleção. É por isso que o Java8 lança o erro.

De acordo com o Java7, sua inferência de tipos é um pouco mais fraca. Então ele tenta fazer algo parecido,

 Base base = new Consumer().get(); set(new Derived(), base); 

E, novamente, não é possível encontrar o método correto que considera Derived, Base como argumentos. Por isso, gera erro, mas por motivos diferentes:

  set(new Derived(), new Consumer().get()); ^ method Consumer.set(Base,Derived) is not applicable (argument mismatch; Base cannot be converted to Derived) method Consumer.set(Derived,Collection< ? extends Consumer>) is not applicabl e (argument mismatch; Base cannot be converted to Collection< ? extends Consu mer>) 

PS: Obrigado a Holger, por apontar a incompletude da minha resposta.