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?
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>
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With