Por que o compilador escolhe esse método genérico com um parâmetro de tipo de classe quando chamado com um tipo de interface não relacionado?

11

Considere as duas classes e a interface a seguir:

public class Class1 {}
public class Class2 {}
public interface Interface1 {}

Por que a segunda chamada para mandatoryinvocar o método sobrecarregado com Class2, se getInterface1e Interface1não tem relacionamento Class2?

public class Test {

    public static void main(String[] args) {
        Class1 class1 = getClass1();
        Interface1 interface1 = getInterface1();

        mandatory(getClass1());     // prints "T is not class2"
        mandatory(getInterface1()); // prints "T is class2"
        mandatory(class1);          // prints "T is not class2"
        mandatory(interface1);      // prints "T is not class2"
    }

    public static <T> void mandatory(T o) {
        System.out.println("T is not class2");
    }

    public static <T extends Class2> void mandatory(T o) {
        System.out.println("T is class2");
    }

    public static <T extends Class1> T getClass1() {
        return null;
    }

    public static <T extends Interface1> T getInterface1() {
        return null;
    }
}

Entendo que o Java 8 quebrou a compatibilidade com o Java 7:

$ /usr/lib/jvm/java-8-openjdk-amd64/bin/javac -source 1.7 -target 1.7 *java; /usr/lib/jvm/java-8-openjdk-amd64/bin/java Test
warning: [options] bootstrap class path not set in conjunction with -source 1.7
1 warning
T is not class2
T is not class2
T is not class2
T is not class2

E com Java 8 (também testado com 11 e 13):

$ /usr/lib/jvm/java-8-openjdk-amd64/bin/javac *java; /usr/lib/jvm/java-8-openjdk-amd64/bin/java Test                        
T is not class2
T is class2
T is not class2
T is not class2
froque
fonte
11
Conclusão: a sobrecarga de método em Java traz tantas surpresas que deve ser usada com extremo cuidado. Discriminar duas sobrecargas apenas por um limite de um tipo de parâmetro está causando problemas, conforme demonstrado pela complexidade da resposta. Você está basicamente pedindo a cada leitor do seu código que leia e entenda a resposta antes que eles possam entender o seu código. Em outras palavras: se o seu programa for interrompido quando a inferência de tipo for aprimorada, você não estará em território seguro. Boa sorte!
Stephan Herrmann

Respostas:

4

As regras de inferência de tipo receberam uma revisão significativa no Java 8, principalmente a inferência de tipo de destino foi muito aprimorada. Portanto, enquanto antes do Java 8 o site de argumentos do método não recebia nenhuma inferência, padronizando o tipo apagado ( Class1para getClass1()e Interface1para getInterface1()), no Java 8 o tipo aplicável mais específico é inferido. O JLS para Java 8 introduziu um novo capítulo, capítulo 18. Inferência de tipo que está faltando no JLS for Java 7.


O tipo aplicável mais específico para <T extends Interface1>é <X extends RequiredClass & BottomInterface>, onde RequiredClassé uma classe requerida por um contexto e BottomInterfaceé um tipo inferior para todas as interfaces (inclusive Interface1).

Nota: Cada tipo Java pode ser representado como SomeClass & SomeInterfaces. Uma vez que RequiredClassé subtipo de SomeClass, e BottomInterfaceé subtipo de SomeInterfaces, Xé subtipo de todo tipo Java. Portanto, Xé um tipo inferior de Java.

Xcorresponde a ambos os public static <T> void mandatory(T o)e public static <T extends Class2> void mandatory(T o)métodos assinaturas desde Xé Java tipo de fundo.

Portanto, de acordo com §15.12.2 , mandatory(getInterface1())chama a sobrecarga mais específica do mandatory()método, que é public static <T extends Class2> void mandatory(T o)uma vez que <T extends Class2>é mais específica que <T>.

Aqui está como você pode especificar explicitamente o getInterface1()parâmetro type para fazê-lo retornar o resultado que corresponde à public static <T extends Class2> void mandatory(T o)assinatura do método:

public static <T extends Class2 & Interface1> void helper() {
    mandatory(Test.<T>getInterface1()); // prints "T is class2"
}

O tipo aplicável mais específico para <T extends Class1>é <Y extends Class1 & BottomInterface>, onde BottomInterfaceé um tipo inferior para todas as interfaces.

Ycorresponde à public static <T> void mandatory(T o)assinatura do método, mas não corresponde à public static <T extends Class2> void mandatory(T o)assinatura do método, pois Ynão se estende Class2.

Então mandatory(getClass1())chama public static <T> void mandatory(T o)método.

Ao contrário de getInterface1(), você não pode especificar explicitamente o getClass1()parâmetro type para fazê-lo retornar o resultado que corresponde à public static <T extends Class2> void mandatory(T o)assinatura do método:

                       java: interface expected here
                                     
public static <T extends Class1 & C̲l̲a̲s̲s̲2> void helper() {
    mandatory(Test.<T>getClass1());
}
Bananon
fonte