7
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

Thiyagu
  • 17,362
  • 5
  • 42
  • 79

4 Answers4

4

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.

Community
  • 1
  • 1
Tom
  • 16,842
  • 17
  • 45
  • 54
3

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.

sisyphus
  • 6,174
  • 1
  • 25
  • 35
  • So the reason that this problem doesn't occur when I try to call `b1.getElement()` is that println with Object parameter is called? – Thiyagu Mar 26 '16 at 10:02
  • Yes, b1 is also incorrectly typed but at runtime it doesn't matter because it gets cast to Object. – sisyphus Mar 26 '16 at 11:06
1

At compile time the compiler knows the type and links the call of System.out.println(..) to the method with the correct parameter type. In the first case the compiler resolves the call to println(Object). Because b1.getElement() returns an Object, String is an Object, the method call is correct and raises no exception. In the second case the compiler resolves the call to println(String), because of Box<String>, but b2.getElement() returns an Integer. This cannot be cast to String and a ClassCastException is raised.

Martin
  • 522
  • 4
  • 7
0

The method println is not defined for Integer parameter so your code will call println(Object object) which will call object.toString() to get the string it needs to print out. There is no type checking because everything is an Object.

In the second case your code wants to call println(String someString) and because of that it will check if someString is really a String and because it is not it is going to throw an exception.

CodesInTheDark
  • 151
  • 1
  • 9