¿Por qué el compilador elige este método genérico con un parámetro de tipo de clase cuando se invoca con un tipo de interfaz no relacionado?

11

Considere las siguientes dos clases e interfaces:

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

¿Por qué la segunda llamada para mandatoryinvocar el método sobrecargado con Class2, si getInterface1y Interface1no tiene relación 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;
    }
}

Entiendo que Java 8 rompió la compatibilidad con 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

Y con Java 8 (también probado con 11 y 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
fuente
1
En pocas palabras: la sobrecarga de métodos en Java trae tantas sorpresas que solo debe usarse con extremo cuidado. Discriminar dos sobrecargas solo por un límite de un parámetro de tipo es pedir problemas, como lo demuestra la complejidad de la respuesta. Básicamente, le está pidiendo a cada lector de su código que lea y comprenda esa respuesta antes de que puedan entender su código. Dicho de otra manera: si su programa se interrumpe cuando se mejora la inferencia de tipos, no está en territorio seguro. ¡Buena suerte!
Stephan Herrmann

Respuestas:

4

Las reglas de inferencia de tipos han recibido una revisión importante en Java 8, más notablemente la inferencia de tipos de destino se ha mejorado mucho. Entonces, mientras que antes de Java 8 el sitio de argumento del método no recibió ninguna inferencia, por defecto al tipo borrado ( Class1por getClass1()y Interface1para getInterface1()), en Java 8 se infiere el tipo aplicable más específico. JLS para Java 8 introdujo un nuevo capítulo Capítulo 18. Inferencia de tipo que falta en JLS para Java 7.


El tipo aplicable más específico para <T extends Interface1>es <X extends RequiredClass & BottomInterface>, donde RequiredClasses una clase requerida por un contexto, y BottomInterfacees un tipo inferior para todas las interfaces (incluidas Interface1).

Nota: cada tipo de Java se puede representar como SomeClass & SomeInterfaces. Dado que RequiredClasses un subtipo de SomeClass, y BottomInterfacees un subtipo de SomeInterfaces, Xes un subtipo de cada tipo de Java. Por lo tanto, Xes un tipo de fondo Java.

Xpartidos ambos public static <T> void mandatory(T o)y public static <T extends Class2> void mandatory(T o)métodos firmas ya Xes Java tipo de fondo.

Así, de acuerdo con §15.12.2 , mandatory(getInterface1())llama a la sobrecarga específica mayor parte de mandatory()método, que es public static <T extends Class2> void mandatory(T o)ya <T extends Class2>es más específico que <T>.

Aquí se explica cómo puede especificar explícitamente el getInterface1()parámetro de tipo para que devuelva el resultado que coincide con la public static <T extends Class2> void mandatory(T o)firma del método:

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

El tipo aplicable más específico para <T extends Class1>es <Y extends Class1 & BottomInterface>, donde BottomInterfacees un tipo inferior para todas las interfaces.

Ycoincide con la public static <T> void mandatory(T o)firma del método, pero no coincide con la public static <T extends Class2> void mandatory(T o)firma del método ya Yque no se extiende Class2.

Entonces mandatory(getClass1())llama public static <T> void mandatory(T o)método.

A diferencia de getInterface1(), no puede especificar explícitamente el getClass1()parámetro de tipo para que devuelva el resultado que coincide con la public static <T extends Class2> void mandatory(T o)firma del método:

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