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开发者_如何学JAVA:
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.
Interesting question.
I confirmed your result by compiling it for myself and indeed it does compile (with warnings) if you add the unused type parameter. However, it fails to compile again if you actually specify a type for the type parameter:
new Test<Object>().test(animalList);
new Test<Object>().test(mammalList);
new Test<Object>().test(dogList);
My suspicion is that, because you are using an unchecked operation to construct the Test object, the compiler doesn't bother to check the other parameter types and treats the whole thing as unchecked/unsafe. When you specify the type it reverts back to its previous behaviour.
If you add <T>
to your Test class and then populate the generic with something the compile error will return
new Test<String>().test(mammalList);
My guess is that because Test generic was not defined, the compiler decides it doesn't have enough information to check anything beyond that level.
精彩评论