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

Rob*_*arl 13 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我期待它抛出一个,ClassCastException但它成功打印:

Test: value
Run Code Online (Sandbox Code Playgroud)

它为什么不扔?

Cyä*_*gha 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));
}
Run Code Online (Sandbox Code Playgroud)


rge*_*man 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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

有没有checkcast操作,而append(Object)被称为上隐StringBuilder,因为类型擦除后,T只是Object无论如何.


Ben*_*ale 5

你没有得到a,ClassCastException因为你从地图返回值的上下文不需要编译器在这一点上执行检查,因为它等同于将值赋给类型的变量Object(参见rgettman的答案).

致电:

test.get("test", Double.class);
Run Code Online (Sandbox Code Playgroud)

是使用+运算符的字符串连接操作的一部分.从地图返回的对象只是被视为一个对象Object.为了显示返回的'对象'作为StringtoString()方法的调用是必需的,因为这是一个Object不需要强制转换的方法.

如果你把调用带到test.get("test", Double.class);字符串连接的上下文之外,你会发现它不起作用,即

这不编译:

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

但这样做:

String val = test.get("test", Double.class).toString();
Run Code Online (Sandbox Code Playgroud)

换句话说,你的代码:

System.out.println("Test: " + test.get("test", Double.class));
Run Code Online (Sandbox Code Playgroud)

相当于:

Object obj = test.get("test", Double.class);   
System.out.println("Test: " + obj);
Run Code Online (Sandbox Code Playgroud)

要么:

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


new*_*cct 4

考虑删除类型参数(类型擦除)后类的样子是有启发性的:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

棘手的部分是这一行:

System.out.println("Test: " + test.get("test", Double.class));
Run Code Online (Sandbox Code Playgroud)

如果你这样做了:

Double foo = test.get("test", Double.class);
Run Code Online (Sandbox Code Playgroud)

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

Double foo = (Double)test.get("test", Double.class);
Run Code Online (Sandbox Code Playgroud)

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

System.out.println("Test: " + (Double)test.get("test", Double.class));
Run Code Online (Sandbox Code Playgroud)

但是,它不会插入强制转换,因为强制转换对于编译和正确运行来说并不是必需的,因为字符串连接 ( +) 以相同的方式作用于所有对象;它只需要知道类型是Object,而不是特定的子类。因此,编译器可以省略不必要的强制转换,在本例中确实如此。