I thought that i have some good understanding of Java generics.
This code DOES NOT COMPILE and I know why.
We can pass to test method only List of type Animal or its super type (like List of Objects)
package scjp.examples.generics.wildcards;
import java.util.ArrayList;
import java.util.List;
class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}
public class Test {
public void test(List<? super Animal> col) {
col.add(new Animal());
col.add(new Mammal());
col.add(new Dog());
}
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<Animal>();
List<Mammal> mammalList = new ArrayList<Mammal>();
List<Dog> dogList = new ArrayList<Dog>();
new Test().test(animalList);
new Test().test(mammalList); // Error: The method test(List<? super Animal>) in the type Test is not applicable for the arguments (List<Mammal>)
new Test().test(dogList); // Error: The method test(List<? super Animal>) in the type Test is not applicable for the arguments (List<Dog>)
Dog dog = dogList.get(0);
}
}
But here comes the strange part (at least for me).
If we declare class Test as generic by only adding <T>, then it COMPILES! and throws java.lang.ClassCastException:
public class Test<T> {
...
}
,
Exception in thread "main" java.lang.ClassCastException: scjp.examples.generics.wildcards.Animal cannot be cast to scjp.examples.generics.wildcards.Dog
My question is why adding generic class type <T> (which is not used anywhere) caused class to compile and changed wildcard behaviour?
The expression new Test()
is of raw type. The Java Language Specification defines the types of members of raw types as follows:
The type of a constructor (§8.8), instance method (§8.8, §9.4), or non-static field (§8.3) M of a raw type C that is not inherited from its superclasses or superinterfaces is the erasure of its type in the generic declaration corresponding to C. The type of a static member of a raw type C is the same as its type in the generic declaration corresponding to C.
The erasure of List<? super Animal>
is List
.
The rationale behind this definition is probably that raw types are intended as a means to use generic types from non-generic legacy code, where type parameters are never present. They were not designed, and less than optimal for, leaving a type parameter unspecified; that's what wildcard types are for, i.e. if you code for a compiler compliance level greater than 1.5 you should write
Test<?> test = makeTest();
test.test(animalList);
test.test(mammalList);
test.test(dogList);
and rejoice (or curse, as the matter may be) on seing the compilation errors again.
You have parameterized the type by adding:
public class Test<T> {
But then you use it as the raw type by doing:
new Test()
So all bets are off now. To enable interoperability with legacy code the compiler is letting it through, but it's not type checking now. It will generate a compiler warning though.
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