Search code examples
javagenericstype-erasuregeneric-collections

Java - Obtaining generic object as String Generic type throws exception


public class Box<T> {
    private T element;

    public T getElement() {
        return element;
    }

    public void setElement(T element) {
        this.element = element;
    }
}

public class Test  {

    public static void main(String[] args) {
        List<Box> l = new ArrayList<>(); //Just List of Box with no specific type
        Box<String> box1 = new Box<>();
        box1.setElement("aa");
        Box<Integer> box2 = new Box<>();
        box2.setElement(10);

        l.add(box1);
        l.add(box2);

        //Case 1
        Box<Integer> b1 = l.get(0);
        System.out.println(b1.getElement()); //why no error

        //Case 2
        Box<String> b2 = l.get(1);
        System.out.println(b2.getElement()); //throws ClassCastException

    }
}

The list l holds element of type Box. In case 1, I get the first element as Box<Integer> and in second case the second element in the list is obtained as Box<String>. The ClassCastException is not thrown in the first case.

When I tried to debug, the element's type in b1 and b2 are String and Integer respectively.

Is it related to type erasure?

Ideone link


Solution

  • To be precise, the problem is PrintStream#println.

    Let's check the compiled code using javap -c Test.class:

    72: invokevirtual #12        // Method blub/Box.getElement:()Ljava/lang/Object;
    75: invokevirtual #13        // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    

    As you can see the compiler erased the types and also omitted a cast for Integer, because it wasn't necessary here. The compiler already linked the used overloaded methoded to PrintStream#(Object). It does that due to the JLS rule §5.3:

    Method invocation conversion is applied to each argument value in a method or constructor invocation (§8.8.7.1, §15.9, §15.12): the type of the argument expression must be converted to the type of the corresponding parameter.

    Method invocation contexts allow the use of one of the following:

    • an identity conversion (§5.1.1)
    • a widening primitive conversion (§5.1.2)
    • a widening reference conversion (§5.1.5)
    • a boxing conversion (§5.1.7) optionally followed by widening reference conversion
    • an unboxing conversion (§5.1.8) optionally followed by a widening primitive conversion.

    The third rule is the conversion from a subtype to a supertype:

    A widening reference conversion exists from any reference type S to any reference type T, provided S is a subtype (§4.10) of T.

    And is done before the check if the type can be unboxed (the fifth check: "an unboxing conversion"). So the compiler checks that Integer is a subtype of Object and therefore it has to call #println(Object) (your IDE will tell you the same if you check the called overloaded version).

    The second version on the other hand:

     95: invokevirtual #12        // Method blub/Box.getElement:()Ljava/lang/Object;
     98: checkcast     #14        // class java/lang/String
    101: invokevirtual #15        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    

    has a checkcast to check of the retrieved type of Box#getElement really is a String. This is necessary, because your told the compiler it will be a String (due to the generic type Box<String> b2 = l.get(1);) and it linked the method PrintStream#(String). This check fails with the mentioned ClassCastException, because an Integer cannot be cast to String.