在转换为与实际类不同的泛型类型时,没有ClassCastException

时间:2014-09-12 16:22:05

标签: java generics classcastexception

我有一些看起来像这样的代码(方法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);
    }
}

我原以为它会抛出一个ClassCastException,但它会成功打印出来:

Test: value

为什么不扔?

5 个答案:

答案 0 :(得分:17)

那是因为您正在转换为泛型类型T,它在运行时被擦除,就像所有Java泛型一样。那么在运行时实际发生的事情就是你要转向Object而不是Double

请注意,例如,如果T被定义为<T extends Number>,则您将转为Number(但仍然不是Double)。

如果要进行某些运行时类型检查,则需要使用实际参数clazz(在运行时可用),而不是通用类型T(已擦除)。例如,您可以执行以下操作:

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

答案 1 :(得分:7)

当在返回的“Double”上调用方法并且没有调用任何方法时,我发现字节代码有所不同。

例如,如果您在返回的“Double”上调用doubleValue()(或甚至getClass()),则会出现ClassCastException。使用javap -c Test,我得到以下字节码:

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;

checkcast操作必须抛出ClassCastException。此外,在隐式StringBuilder中,append(double)也会被调用。

没有致电doubleValue()(或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;

没有checkcast操作,并且在隐式append(Object)上调用了StringBuilder,因为在类型擦除之后,T无论如何都只是Object。< / p>

答案 2 :(得分:5)

您没有得到ClassCastException因为从地图返回值的上下文不需要编译器在此时执行检查,因为它等同于将值赋给变量of输入Object(请参阅rgettman's answer)。

致电:

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

是使用+运算符的字符串连接操作的一部分。从地图返回的对象只是被视为Object。为了将返回的“对象”显示为String,需要调用toString()方法,因为这是Object上的方法,不需要强制转换。

如果您在字符串连接的上下文之外接听test.get("test", Double.class);,则会看到它不起作用,即

这不编译:

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

但这样做:

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

换句话说,你的代码:

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

相当于:

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

或:

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

答案 3 :(得分:4)

在删除类型参数( 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);
    }
}

这会编译并生成您看到的相同结果。

棘手的部分是这一行:

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

如果你这样做了:

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

然后在类型擦除后编译器会插入一个强制转换(因为在类型擦除后test.get()返回Object):

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

类似地,编译器也可以在上面的行中插入一个强制转换,如下所示:

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

但是,它不会插入强制转换,因为强制转换不需要编译并且行为正确,因为字符串连接(+)以相同的方式对所有对象起作用;它只需要知道类型是Object,而不是特定的子类。因此,编译器可以省略不必要的强制转换,在这种情况下也是如此。

答案 4 :(得分:2)

似乎Java无法使用推断类型处理强制转换,但是如果使用Class.cast方法,则get调用将按预期抛出异常:

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

不幸的是,我无法更彻底地解释这一点。

修改:您可能会对此Oracle doc感兴趣。