Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 427
user7 Avatar asked Mar 26 '16 08:03

user7


2 Answers

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.

like image 75
Tom Avatar answered Sep 27 '22 18:09

Tom


Ok, the problem here is that b2 is incorrectly marked as being Box<String> when it's actually Box<Integer> (the type of box2) - so b2.getElement() is typed as String, even though it actually contains an Integer. The compiler tries to call the overloaded println method which takes a String rather than the method which takes an Object and so you get a ClassCastException. The Object version of println does an explicit conversion of its argument to a String (via a call to toString()) but the String version of the method doesn't do that.

The underlying problem is using raw types rather than fully specifying the type parameter for list l - it should have been List<Box<?>>. Then you'd have had b1 and b2 as Box and the right overload of System.out.println would have been chosen.

like image 42
sisyphus Avatar answered Sep 27 '22 18:09

sisyphus