【问题标题】:No ClassCastException when casting to generic type different to actual class [duplicate]转换为与实际类不同的泛型类型时没有 ClassCastException [重复]
【发布时间】:2014-09-13 04:45:53
【问题描述】:

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

为什么不扔?

【问题讨论】:

  • System.out.println 调用打印什么?
  • 我感觉这与为什么不能为泛型类型参数传递原始类型的原因相同。您是否尝试过将Map 中的值从Object 更改为其他值,例如String?也许是因为Object的多态属性而被允许的
  • @mariosangiorgio : 它打印Test: value
  • 有趣的是,test.get("test", Double.class).doubleValue() 确实抛出了ClassCastException
  • 当编译器清楚地警告您演员未检查时,您为什么会期待ClassCastException

标签: java generics classcastexception


【解决方案1】:

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

请注意,例如,如果 T 被定义为 &lt;T extends Number&gt;,您将转换为 Number(但仍不会转换为 Double)。

如果你想做一些运行时类型检查,你需要使用实际参数clazz(在运行时可用),而不是泛型类型T(被擦除)。例如,您可以执行以下操作:

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

【讨论】:

  • 这可能只是一行return clazz.cast(map.get(key));
  • 你说得对——我想我应该检查一下Javadoc,因为这不是我通常需要处理的事情。我将更新我的答案。
  • @LouisWasserman, Cyäegha:请注意,两者在处理null 的方式上略有不同。 class.cast(null) 可以正常工作,而 null.getClass() 会抛出 NullPointerException。
  • 没错,我想这是在这里使用Class.cast 的另一个好理由,因为HashMap.get 可以返回null
【解决方案2】:

我发现在返回的“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

【讨论】:

    【解决方案3】:

    您没有得到ClassCastException,因为您从映射返回值的上下文不需要编译器此时执行检查,因为它相当于将值分配给@类型的变量987654323@(见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);
    

    【讨论】:

      【解决方案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);
          }
      }
      

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

      棘手的部分是这一行:

      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,而不是特定的子类。因此,编译器可以省略不必要的强制转换,在这种情况下确实如此。

      【讨论】:

        【解决方案5】:

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

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

        很遗憾,我无法更彻底地解释这一点。

        编辑:您可能对此Oracle doc 感兴趣。

        【讨论】:

          猜你喜欢
          • 2017-09-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-09-16
          • 1970-01-01
          • 2019-07-14
          相关资源
          最近更新 更多