javagenericsoverload-resolutionraw-types

Why does class extending raw type result in overload ambiguity?


Why is method(new ExtendsRaw()) ambiguous in the code below?

public class Main {
    private Main() {}

    static class Generic<T> {}
    @SuppressWarnings("rawtypes")
    static class ExtendsRaw extends Generic {}

    static void method(Generic<String> generic) {}
    static void method(ExtendsRaw raw) {}

    static void test() {
        method(new Generic<>());
        // Ambiguous method call error
        method(new ExtendsRaw());
    }
}

In other cases, Java doesn't consider it ambiguous when the parameter type of one overload is a subtype of the other overload's parameter. For example:

import java.util.List;
import java.util.ArrayList;

public class Main {
    private Main() {}

    static void method1(Object supertype) {}
    static void method1(String subtype) {}

    static void method2(List<String> supertype) {}
    static void method2(ArrayList<String> subtype) {}

    static void test() {
        // These both work fine.
        method1(new Object());
        method1("string");

        // These also both work fine.
        method2(List.of());
        method2(new ArrayList<>());
    }
}

What's the reason for this behavior?


Solution

  • TL;DR: Java's spec considers ExtendsRaw more specific than Generic, but not more specific than Generic<String>, so overload resolution is ambiguous.


    I researched the Java spec and came across section 15.12.2.5. Choosing the Most Specific Method, which says:

    One applicable method m1 is more specific than another applicable method m2, for an invocation with argument expressions e1, ..., ek, if any of the following are true:

    Neither method in my code snippet is generic, so the following bullet point applies:

    m2 is not generic, and m1 and m2 are applicable by strict or loose invocation, and where m1 has formal parameter types S1, ..., Sn and m2 has formal parameter types T1, ..., Tn, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ n, n = k).

    This means my code snippet should work if new ExtendsRaw() is "more specific" for ExtendsRaw than it is for Generic<String>.

    The spec then says:

    A type S is more specific than a type T for any expression if S <: T (§4.10).

    So the question becomes whether ExtendsRaw <: Generic<String> is true. The linked section says:

    The subtype and supertype relations are binary relations on types.

    The supertypes of a type are obtained by reflexive and transitive closure over the direct supertype relation, written S >1 T, which is defined by rules given later in this section. We write S :> T to indicate that the supertype relation holds between S and T.

    S is a proper supertype of T, written S > T, if S :> T and S ≠ T.

    The subtypes of a type T are all types U such that T is a supertype of U, and the null type. We write T <: S to indicate that that the subtype relation holds between types T and S.

    The last part means that S :> T (S is a proper supertype of T) implies T <: S (T is a proper subtype of S).

    The following subsection, 4.10.2 says:

    Given a non-generic class or interface C, the direct supertypes of the type of C are all of the following:

    • The direct superclass type of C (§8.1.4), if C is a class.
    • The direct superinterface types of C (§8.1.5, §9.1.3).
    • The type Object, if C is an interface with no direct superinterface types (§9.1.3).

    ExtendsRaw is a non-generic class so the first bullet point implies that Generic (the raw type) is one if its direct supertypes, which means ExtendsRaw <: Generic.

    So new ExtendsRaw() is "more specific" for ExtendsRaw than it is for Generic. The following code compiles and confirms it:

    public class Main {
        private Main() {}
    
        static class Generic<T> {}
        @SuppressWarnings("rawtypes")
        static class ExtendsRaw extends Generic {}
    
        static void method(Generic generic) {}
        static void method(ExtendsRaw raw) {}
    
        static void test() {
            // This resolves to `method(Generic generic)`
            method(new Generic());
            // This resolves to `method(ExtendsRaw raw)`
            method(new ExtendsRaw());
        }
    }
    

    None of the other bullet points establish that ExtendsRaw <: Generic<String> is true, which is what we're trying to do.

    <: is transitive (see above) so if we can prove Generic <: Generic<String> is true, then that would prove ExtendsRaw <: Generic<String> is true.

    Section 4.10.2 continues and says:

    Given a generic class or interface C with type parameters F1,...,Fn (n > 0), the direct supertypes of the raw type C (§4.8) are all of the following:

    • The erasure (§4.6) of the direct superclass type of C, if C is a class.
    • The erasure of the direct superinterface types of C.
    • The type Object, if C is an interface with no direct superinterface types.

    Generic<T> is a generic class and Generic is its raw type so the first bullet point implies that Generic <: |Generic| is true (|Generic| is the erasure of Generic<T>).

    None of the other bullet points help make progress, so as far as I can tell, there's nowhere to go from here. We cannot prove that Generic<String> is a direct supertype of |Generic|, which means we cannot prove Generic<String> is a proper supertype of ExtendsRaw.

    That's why the overload resolution is ambiguous. The logic in the spec considers ExtendsRaw more specific than Generic, but not more specific than Generic<String>.

    This is counterintuitive because ExtendsRaw is assignable to Generic<String>, but the overload resolution logic isn't based on assignability.


    Bonus: Why does the List<String> and ArrayList<String> scenario work?

    Section 4.10.2 says:

    Given a generic class or interface C with type parameters F1,...,Fn (n > 0), the direct supertypes of the parameterized type C<T1,...,Tn>, where each of Ti (1 ≤ i ≤ n) is a type, are all of the following:

    • The substitution [F1:=T1,...,Fn:=Tn] applied to the direct superclass type of C, if C is a class.
    • The substitution [F1:=T1,...,Fn:=Tn] applied to the direct superinterface types of C.
    • C<S1,...,Sn>, where Si contains Ti (1 ≤ i ≤ n) (§4.5.1).
    • The type Object, if C is an interface with no direct superinterface types.
    • The raw type C.

    ArrayList<String> is a generic class with one type parameter: String. List is a direct superinterface of ArrayList so the second bullet point implies that List<String> is a direct supertype of ArrayList<String>.

    That means ArrayList<String> <: List<String> is true, which means that new ArrayList<>() in my code snippet is more specific for ArrayList<String> than List<String>.