Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which of the overloaded methods will be called on runtime if we apply type erasure, and why?

Suppose we have the following generic class

public class SomeType<T> {
    public <E> void test(Collection<E> collection){
        System.out.println("1st method");

        for (E e : collection){
            System.out.println(e);
        }
    }

    public void test(List<Integer> integerList){
        System.out.println("2nd method");

        for (Integer integer : integerList){
            System.out.println(integer);
        }
    }

}

Now inside main method we have the following code snippet

SomeType someType = new SomeType();
List<String> list = Arrays.asList("value");
someType.test(list);

As a result of executing someType.test(list) we will get "2nd method" in our console as well as java.lang.ClassCastException. As I understand, the reason of why second test method being executed is that we don't use generics for SomeType. So, compiler instantly removes all generics information from the class (i.e. both <T> and <E>). After doing that second test method will have List integerList as a parameter and of course List matches better to List than to Collection.

Now consider that inside main method we have the following code snippet

SomeType<?> someType = new SomeType<>();
List<String> list = Arrays.asList("value");
someType.test(list);

In this case we will get "1st method" in the console. It means that first test method being executed. The question is why?

From my understanding on runtime we never have any generics information because of type erasure. So, why then second test method cannot be executed. For me second test method should be (on runtime) in the following form public void test(List<Integer> integerList){...} Isn't it?

like image 436
ruvinbsu Avatar asked Dec 16 '15 07:12

ruvinbsu


2 Answers

Applicable methods are matched before type erasure (see JSL 15.12.2.3). (Erasure means that runtime types are not parameterized, but the method was chosen at compile time, when type parameters were available)

The type of list is List<String>, therefore:

  • test(Collection<E>) is applicable, because List<Integer> is compatible with Collection<E>, where E is Integer (formally, the constraint formula List<Integer> → Collection<E> [E:=Integer] reduces to true, because List<Integer> is a subtype of Collection<Integer>).

  • test(List<String>) is not applicable, because List<String> is not compatible with List<Integer> (formally, the constraint formula List<String>List<Integer> reduces to false because String is not a supertype of Integer).

The details are explained hidden in JSL 18.5.1.

For test(Collection<E>):

Let θ be the substitution [E:=Integer]

[...]

A set of constraint formulas, C, is constructed as follows: let F1, ..., Fn be the formal parameter types of m, and let e1, ..., ek be the actual argument expressions of the invocation.

In this case, we have F1 = Collection<E> and e1 = List<Integer>

Then: [the set of constraint formulas] includes ‹ei → Fi θ›

In this case, we have List<Integer> → Collection<E> [E:=Integer] (where → means that e1 is compatible with F1 after the type-variable E has been inferred)

For test(List<String>), there is no substitution (because there are no inference variables) and the constraint is just List<String>List<Integer>.

like image 126
Javier Avatar answered Sep 18 '22 09:09

Javier


The JLS is a bit of a rat's nest on this one, but there is an informal (their words, not mine) rule that you can use:

[O]ne method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time error.

For the sake of argument, let's call <E> test(Collection<E>) method 1, and test(List<Integer>) method 2.

Let's throw a spanner in here - we know that this entire class is generic, so instantiation of it without a type of some kind produces... less than desirable type checks at runtime.

The other part to this is due to the fact that List is more specific than Collection, if a method is passed a List, it will seek to accommodate that more readily than a Collection, with the caveat that the type should be checked at compile time. Since it isn't with that raw type, I believe that this particular check is skipped, and Java is treating List<Integer> as more specific than Collection<capture(String)>.

You should file a bug with the JVM, since this appears to be inconsistent. Or, at least, have the folks who penned the JLS explain why this is legal in slightly better English than their wonky math notation...

Moving on; with your second example, you give us the courtesy of typing your instance as a wildcard, which allows Java to make the correct compile-time assertion that test(Collection<E>) is the safest method to choose.

Note that none of these checks happen at runtime. These are all decisions made before Java runs, as ambiguous method calls or a call to a method with an unsupported parameter results in a compile time error.

Moral of the story: don't use raw types. They're evil. It makes the type system behave in strange ways, and it's really only there to maintain backwards compatibility.

like image 29
Makoto Avatar answered Sep 18 '22 09:09

Makoto