Почему компилятор выбирает этот универсальный метод с параметром типа класса при вызове с несвязанным типом интерфейса?

11

Рассмотрим следующие два класса и интерфейс:

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

Почему второй вызов mandatoryвызывает перегруженный метод с Class2, если getInterface1и Interface1не имеет отношения с 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;
    }
}

Я понимаю, что Java 8 нарушила совместимость с 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

И с Java 8 (также протестировано с 11 и 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
источник
1
Итог: перегрузка методов в Java приносит так много сюрпризов, что их следует использовать только с особой осторожностью. Различение двух перегрузок только по границе параметра типа вызывает проблемы, о чем свидетельствует сложность ответа. Вы в основном просите каждого читателя вашего кода прочитать и понять этот ответ, прежде чем они смогут понять ваш код. Другими словами: если ваша программа ломается при улучшении вывода типов, вы не на безопасной территории. Удачи!
Стефан Херрманн

Ответы:

4

Правила вывода типов получили существенную переработку в Java 8, в частности, вывод целевого типа был значительно улучшен. Таким образом, в то время как до Java 8 сайт с аргументом метода не получал никакого вывода, по умолчанию стертый тип ( Class1для getClass1()и Interface1для getInterface1()), в Java 8 выводится наиболее конкретный применимый тип. JLS для Java 8 представил новую главу Глава 18. Вывод типа, который отсутствует в JLS для Java 7.


Наиболее конкретный применимый тип для <T extends Interface1>- это <X extends RequiredClass & BottomInterface>, где RequiredClassкласс, требуемый контекстом, и BottomInterfaceнижний тип для всех интерфейсов (включая Interface1).

Примечание. Каждый тип Java может быть представлен как SomeClass & SomeInterfaces. Так как RequiredClassявляется подтипом SomeClassи BottomInterfaceявляется подтипом SomeInterfaces, Xявляется подтипом каждого типа Java. Таким образом, Xявляется типом дна Java.

Xсоответствует обеим сигнатурам public static <T> void mandatory(T o)и public static <T extends Class2> void mandatory(T o)сигнатурам методов, поскольку Xявляется типом дна Java.

Так, по словам §15.12.2 , mandatory(getInterface1())вызывает наиболее специфические перегружать mandatory()метод, который с public static <T extends Class2> void mandatory(T o)тех пор <T extends Class2>является более конкретным , чем <T>.

Вот как вы можете явно указать getInterface1()параметр типа, чтобы он возвращал результат, соответствующий public static <T extends Class2> void mandatory(T o)сигнатуре метода:

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

Наиболее конкретный применимый тип для <T extends Class1>- это <Y extends Class1 & BottomInterface>где BottomInterfaceнижний тип для всех интерфейсов.

Yсоответствует public static <T> void mandatory(T o)сигнатуре метода, но не соответствует public static <T extends Class2> void mandatory(T o)сигнатуре метода, поскольку Yне расширяется Class2.

Так mandatory(getClass1())называет public static <T> void mandatory(T o)метод.

В отличие от with getInterface1(), вы не можете явно указать getClass1()параметр типа, чтобы он возвращал результат, соответствующий public static <T extends Class2> void mandatory(T o)сигнатуре метода:

                       java: interface expected here
                                     
public static <T extends Class1 & C̲l̲a̲s̲s̲2> void helper() {
    mandatory(Test.<T>getClass1());
}
Bananon
источник