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

5

14

I have some code which looks something like this (part of a negative test of the method get):

import java.util.*;
public class Test {
    Map<String, Object> map = new HashMap<>();
    public static void main (String ... args) {
        Test test = new Test();
        test.put("test", "value"); // Store a String
        System.out.println("Test: " + test.get("test", Double.class)); // Retrieve it as a Double
    }

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

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

I was expecting it to throw a ClassCastException but it runs through successfully printing:

Test: value

Why doesn't it throw?

Burroughs answered 12/9, 2014 at 16:22 Comment(8)
What does the System.out.println call print?Thorsten
I have a feeling it's linked to the same reason of why you cannot pass a primitive type for a generic type argument. Have you tried changing the values in the Map from Object to something else, such as String? Maybe it's allowed due to the polymorphic property of ObjectDeclarer
@Thorsten : It prints Test: value.Pheon
Interestingly, test.get("test", Double.class).doubleValue() does throw a ClassCastException.Fanion
@VinceEmigh same result with Map<String, String>Burroughs
Why would you expect a ClassCastException when the compiler clearly warns you that the cast is unchecked?Annamarieannamese
https://mcmap.net/q/753423/-java-generics-with-class-lt-t-gt I have a feeling the Class<T> clazz is not doing what you thinkEvelynneven
@Sebas: Well, it's not doing anything -- it's not usedBlear
B
4

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.

Blear answered 12/9, 2014 at 20:45 Comment(0)
H
17

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));
}
Hesterhesther answered 12/9, 2014 at 16:39 Comment(4)
This could just be the one line return clazz.cast(map.get(key));Viscometer
You're right - I guess I should have checked the Javadoc, since it's not the sort of thing I commonly have to deal with. I'm going to update my answer.Constabulary
@LouisWasserman, Cyäegha: note that both differ a bit by how they handle null. class.cast(null) works without problems, while null.getClass() throws a NullPointerException.Returnee
True, I guess that's another good reason to use Class.cast here, since HashMap.get can return null.Constabulary
F
8

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.

Fanion answered 12/9, 2014 at 16:43 Comment(0)
I
5

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);
Insouciance answered 12/9, 2014 at 16:40 Comment(0)
B
4

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.

Blear answered 12/9, 2014 at 20:45 Comment(0)
P
2

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.

Pheon answered 12/9, 2014 at 16:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.