Search code examples
javagenericsjava-8language-lawyerjavac

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


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

Solution

  • 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());
    }