No ClassCastException when casting to generic type different to actual class [duplicate]

纵饮孤独 提交于 2019-12-03 05:45:21

It is instructive to consider what the class looks like after removing type parameters (type erasure):

public class Test {
    Map map = new HashMap();
    public static void main (String ... args) {
        Test test = new Test();
        test.put("test", "value");
        System.out.println("Test: " + test.get("test", Double.class));
    }

    public Object get(String key, Class clazz) {
        return map.get(key);
    }

    public void put(String key, Object value) {
        map.put(key, value);
    }
}

This compiles and produces the same result that you see.

The tricky part is this line:

System.out.println("Test: " + test.get("test", Double.class));

If you had done this:

Double foo = test.get("test", Double.class);

then after type erasure the compiler would have inserted a cast (because after type erasure test.get() returns Object):

Double foo = (Double)test.get("test", Double.class);

So analogously, the compiler could have inserted a cast in the above line too, like this:

System.out.println("Test: " + (Double)test.get("test", Double.class));

However, it doesn't insert a cast, because the cast is not necessary for it to compile and behave correctly, since string concatenation (+) works on all objects the same way; it only needs to know the type is Object, not a specific subclass. Therefore, the compiler can omit an unnecessary cast and it does in this case.

That's because you are casting to the generic type T, which is erased at runtime, like all Java generics. So what actually happens at runtime, is that you are casting to Object and not to Double.

Note that, for example, if T was defined as <T extends Number>, you would be casting to Number (but still not to Double).

If you want to do some runtime type checking, you need to use the actual parameter clazz (which is available at runtime), and not the generic type T (which is erased). For instance, you could do something like:

public <T> T get(String key, Class<T> clazz) {
    return clazz.cast(map.get(key));
}

I found a difference in the byte code when a method is called on the returned "Double" and when no method is called.

For example, if you were to call doubleValue() (or even getClass()) on the returned "Double", then the ClassCastException occurs. Using javap -c Test, I get the following bytecode:

34: ldc           #15                 // class java/lang/Double
36: invokevirtual #16 // Method get (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
39: checkcast     #15                 // class java/lang/Double
42: invokevirtual #17                 // Method java/lang/Double.doubleValue:()D
45: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder;

The checkcast operation must be throwing the ClassCastException. Also, in the implicit StringBuilder, append(double) would have been called.

Without a call to doubleValue() (or getClass()):

34: ldc           #15                 // class java/lang/Double
36: invokevirtual #16 // Method get:(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
39: invokevirtual #17 // Method java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;

There is no checkcast operation, and append(Object) is called on the implicit StringBuilder, because after type erasure, T is just Object anyway.

Benjamin Gale

You do not get a ClassCastException because the context in which you are returning the value from the map does not require the compiler to perform a check at this point as it is equivalent to assigning the value to a variable of type Object (See rgettman's answer).

The call to:

test.get("test", Double.class);

is part of a string concatenation operation using the + operator. The object returned from your map is just being treat as if it is an Object. In order to display the returned 'object' as a String a call to the toString() method is required and since this is a method on Object no cast is required.

If you take the call to test.get("test", Double.class); outside of the context of the string concatenation you will see that it does't work i.e.

This does not compile:

// can't assign a Double to a variable of type String...
String val = test.get("test", Double.class);

But this does:

String val = test.get("test", Double.class).toString();

To put it another way, your code:

System.out.println("Test: " + test.get("test", Double.class));

is equivalent to:

Object obj = test.get("test", Double.class);   
System.out.println("Test: " + obj);

or:

Object obj = test.get("test", Double.class);
String value = obj.toString();    
System.out.println("Test: " + value);

It seems that Java is unable to process the cast with an inferred type, however if you use the Class.cast method, the call to get throws an exception as expected :

public <T> T get(String key, Class<T> clazz) {
    return clazz.cast(map.get(key)) ;
}

Unfortunately, I am not able to explain that more thoroughly.

Edit : you might be interested by this Oracle doc.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!