Why does the compiler choose this generic method with a class type parameter when invoked with an unrelated interface type?
Asked Answered
S

1

10

Consider the following two classes and interface:

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

Why does the second call to mandatory invoke the overloaded method with Class2, if getInterface1 and Interface1 have no relationship with 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;
    }
}

I understand that Java 8 broke compatibility with 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

And with Java 8 (also tested with 11 and 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
Seise answered 20/12, 2019 at 18:47 Comment(1)
Bottom line: method overloading in Java brings so many surprises, it should be used with extreme care only. Discriminating two overloads only by a bound of a type parameter is asking for trouble, as demonstrated by the complexity of the answer. You are basically asking each reader of your code to read and understand that answer before they can understand your code. Put differently: if your program breaks when type inference is improved, you are not on safe territory. Good luck!Florrie
C
4

The rules of type inference have received a significant overhaul in Java 8, most notably target type inference has been much improved. So, whereas before Java 8 the method argument site did not receive any inference, defaulting to erased type (Class1 for getClass1() and Interface1 for getInterface1()), in Java 8 the most specific applicable type is inferred. The JLS for Java 8 introduced a new chapter Chapter 18. Type Inference that's missing in JLS for Java 7.


The most specific applicable type for <T extends Interface1> is <X extends RequiredClass & BottomInterface>, where RequiredClass is a class required by a context, and BottomInterface is a bottom type for all interfaces (including Interface1).

Note: Each Java type can be represented as SomeClass & SomeInterfaces. Since RequiredClass is sub-type of SomeClass, and BottomInterface is sub-type of SomeInterfaces, X is sub-type of every Java type. Therefore, X is a Java bottom type.

X matches both public static <T> void mandatory(T o) and public static <T extends Class2> void mandatory(T o) methods signatures since X is Java bottom type.

So, according to §15.12.2, mandatory(getInterface1()) calls the most specific overloading of mandatory() method, which is public static <T extends Class2> void mandatory(T o) since <T extends Class2> is more specific than <T>.

Here is how you can explicitly specify getInterface1() type parameter to make it return the result that matches public static <T extends Class2> void mandatory(T o) method signature:

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

The most specific applicable type for <T extends Class1> is <Y extends Class1 & BottomInterface>, where BottomInterface is a bottom type for all interfaces.

Y matches public static <T> void mandatory(T o) method signature, but it doesn't match public static <T extends Class2> void mandatory(T o) method signature since Y doesn't extend Class2.

So mandatory(getClass1()) calls public static <T> void mandatory(T o) method.

Unlike with getInterface1(), you can't explicitly specify getClass1() type parameter to make it return the result that matches public static <T extends Class2> void mandatory(T o) method signature:

                       java: interface expected here
                                     ↓
public static <T extends Class1 & C̲l̲a̲s̲s̲2> void helper() {
    mandatory(Test.<T>getClass1());
}
Celeriac answered 20/12, 2019 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.