我有一些看起来像这样的代码(方法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
为什么不扔?
答案 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感兴趣。