通用类型转换

时间:2015-08-12 09:20:16

标签: java generics casting

我有以下课程(简化但仍然是一个工作示例):

class Test<T> {
    List<T> l = new ArrayList<>();

    public Test() {
    }

    public void add(Object o) {
        l.add((T)o);
    }
}

测试代码:

Test<Double> t = new Test<>();
t.add(1);
t.add(1.2);
t.add(-5.6e-2);
t.add("hello");

一切都很好,而且这不是我所期待的。不应该add方法抛出ClassCastException吗?如果我添加一个或多或少相同的get方法:

    public T get(int i) {
        return l.get(i);
    }
.../...
t.get(1);             // OK.
t.get(3);             // OK (?)
Double d = t.get(3);  // throws ClassCastException

为什么仅在变量赋值时引发异常?如果(T)演员无效,我该如何强制执行类型一致性?

3 个答案:

答案 0 :(得分:21)

  

add方法不应抛出ClassCastException吗?

不,它不应该(尽管我希望它这样做)。简而言之,Java实现泛型会在编译代码后丢弃类型信息,因此List<T>可以使用任何Object,并且不会检查add方法中的强制转换。

  

为什么仅在变量赋值时抛出异常?

因为编译器插入了Double的强制转换。 Java编译器知道get的返回类型是T,它是Double,所以它插入一个强制转换来匹配变量d的类型,结果正在被分配。

以下是如何实现通用安全转换:

class Test<T> {
    private final Class<T> cl;
    List<T> l = new ArrayList<>();

    public Test(Class<T> c) {
        cl = c;
    }

    public void add(Object o) {
        l.add(cl.cast(o));
    }
}

现在,强制转换由Class<T>对象执行,因此在尝试插入不正确类型的对象时会得到ClassCastException

答案 1 :(得分:7)

作为替代解决方案,您可以使用Collections.checkedList

class Test<T> {
    List<T> l;

    public Test(Class<T> c) {
        l = Collections.checkedList(new ArrayList<T>(), c);
    }

    public void add(Object o) {
        l.add((T) o);
    }
}

这样您将获得以下异常:

Exception in thread "main" java.lang.ClassCastException: Attempt to insert 
  class java.lang.Integer element into collection with element type class java.lang.Double
    at java.util.Collections$CheckedCollection.typeCheck(Collections.java:3037)
    at java.util.Collections$CheckedCollection.add(Collections.java:3080)
    at Test.add(Test.java:13)

答案 2 :(得分:7)

为了完成这个资源,这里是一个强制转换为通用的编译字节码的区别:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: invokeinterface #7,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    10: pop
    11: return

显式转换为Double没有泛型:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class java/lang/Double
     8: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

您可以看到带有泛型的版本根本不执行checkcast指令(感谢type erasure,因此在为数据提供不匹配的类时,不应该发生异常。遗憾的是,这并没有得到更严格的执行,但这是有道理的,因为泛型用于进行更严格的编译时类型检查,并且由于类型擦除而在运行时没有太大帮助。

Java将检查函数参数的类型,以查看是否存在类型匹配,或者是否可以执行类型提升。在您的情况下,String是参数的类型,可以提升为Object,这是确保函数调用有效的编译时类型检查的范围。

有几个选项,而dasblinkenlight的解决方案可能是最优雅的。 (您可能无法更改方法签名,例如,如果要覆盖继承的add方法,或计划传递add方法等)。

另一个可能有用的选项是使用有界类型参数而不是无限制参数。由于类型擦除,无限制类型参数在编译后完全丢失,但使用有界类型参数将用它/必须扩展的那些实例替换泛型类型的实例。

class Test<T extends Number> {

当然,T此时并不是真正的通用,但是使用此类定义将在运行时强制执行类型,因为将根据Number超类检查强制转换 。这是证明它的字节码:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class java/lang/Number
     8: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

此类定义在尝试添加字符串时生成所需的ClassCastException