Java wildcard strange behaviour when class is generic
Asked Answered
A

4

6

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?

Abohm answered 16/5, 2011 at 21:25 Comment(0)
E
7

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.

Eddings answered 16/5, 2011 at 21:39 Comment(2)
+1. I should add, that although it compiles in Eclipse, it's sure produces a bunch of scary warnings, to which you should pay attention.Darius
Actually not just eclipse, that warning is mandated by the spec: "An invocation of a method or constructor of a raw type generates an unchecked warning if erasure changes any of the types of any of the arguments to the method or constructor." In general, nearly all code that might cause heap pollution at runtime is required to generate an unchecked warning at compile time.Eddings
S
1

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.

Siclari answered 16/5, 2011 at 21:39 Comment(0)
M
1

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.

Maddux answered 16/5, 2011 at 21:41 Comment(0)
T
0

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.

Thermography answered 16/5, 2011 at 21:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.