Java会在适用的情况下使用类型擦除,但是,当然,在调用方法时,参数类型必须匹配。
当我在编译时无法确保类型时如何避免使用ClassCastException
?示例:
class KeyedHashSet<K,E> // implements Set<E>
{ final Map<K,E> map = new HashMap<>();
final Function<E,K> keyExtractor; // extracts intrusive key from value
public KeyedHashSet(Function<E,K> keyExtractor)
{ this.keyExtractor = keyExtractor; }
public boolean contains(Object o)
{ if (o != null)
try
{ @SuppressWarnings("unchecked") // <- BAD
K key = keyExtractor.apply((E)o);
E elem = map.get(key);
return elem != null && elem.equals(o);
} catch (ClassCastException ex) // <- EVIL!!!
{}
return false;
}
// more methods
}
方法Set.contains
采用一个任意对象作为参数。但是我无法从任意对象中提取哈希查找所需的密钥。这仅适用于E类型的对象。
实际上,当对象不是E类型时,我对键不感兴趣,因为在这种情况下,我确定该集合不包含该对象。
但是上述解决ClassCastException
的方法有几个缺点:
ClassCastException
实现的深处抛出一个keyExtractor
,这是不想要的。 是否可以在调用o
之前,根据keyExtractor
的参数检查.apply
的类型 不合理的运行时开销?
注意:我知道上面的设计要求E具有不可变的密钥。但这没什么大不了,而且经常发生。
答案 0 :(得分:1)
应用删除后,很难确定未经检查的强制转换是否会触发ClassCastException
。毕竟,类型被简化为Object
;为什么强制转换为Object
会引发强制转换异常?导致此错误的一个原因是调用非通用实现的通用代码,其中明确声明了类型。举个例子:
class Test<K> {
public void foo(Object o) {
bar((K) o);
}
public void bar(K k) {
System.out.println(k);
}
public static void main(String[] args) {
Test<Integer> test = new Test<>();
test.foo("hello");
}
}
即使通用类型参数为"hello"
,以上示例仍将正确打印Integer
。删除之后,方法bar
仅需要一个对象:
public bar(Ljava/lang/Object;)V
如果我们扩展Test
并覆盖显式类型的bar
,则将产生错误。
class TestInteger extends Test<Integer> {
@Override
public void bar(Integer k) {
super.bar(k);
}
public static void main(String[] args) {
Test<Integer> test = new TestInteger();
test.foo("hello");
}
}
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at TestInteger.bar(TestInteger.java:17)
at Test.foo(TestInteger.java:9)
at TestInteger.foo(TestInteger.java:17)
at TestInteger.main(TestInteger.java:24)
在此子类中,被重写的方法与Test<K>
生成的方法具有不同的签名。编译器会创建一个新的重载方法,称为 synthetic 或 bridge方法,以调用bar
中编写的TestInteger
。这种桥接方法就是ClassCastException
发生的地方。看起来像下面这样:
public void bar(Object k) {
bar((Integer) k); //java.lang.String cannot be cast to java.lang.Integer
}
public void bar(Integer k) {
System.out.println(k);
}
在您的示例中,在对keyExtractor.apply((E)o)
的调用中,存在一个签名,该签名依赖于导致强制转换异常的显式类型。
是可以的,但是您必须为KeyedHashSet
类提供额外的数据。您无法直接获取与类型参数关联的Class
对象。
一种方法是将Class
类型注入容器并调用isInstance
:
此方法是Java语言
instanceof
运算符的动态等效项。如果指定的true
参数为非null,则该方法返回Object
,并且可以将其强制转换为该Class
对象表示的引用类型,而无需引发ClassCastException
。否则返回false
。
public class Test<K> {
final Class<K> clazz;
Test(Class<K> clazz) { this.clazz = clazz; }
public void foo(Object o) {
if (clazz.isInstance(o)) {
bar((K) o);
}
}
...
Test<Integer> test = new Test<>(Integer.class);
test.foo("string");
您还可以使用一些验证策略来执行实例检查:
public class Test<K> {
final Function<Object, Boolean> validator;
Test(Function<Object, Boolean> validator) { this.validator = validator; }
public void foo(Object o) {
if (validator.apply(o)) {
bar((K) o);
}
}
...
Test<Integer> test = new Test<>(k -> k instanceof Integer);
test.foo("string");
另一种选择是将类型检查移至Function<E,K> keyExtractor
实例中,并使类型参数变为Function<Object,K> keyExtractor
,如果类型不正确,则返回null
。
从理论上讲,也可以反思地检查keyExtractor
的方法签名并获得Class
实例,但也不保证其实现也将显式定义类型参数。 / em>
isInstance
的执行时间实际上非常快。 There's an interesting article实验性地比较了使用不安全强制转换的尝试捕获速度与isInstance
解决方案。在实验结果中,显式检查类型的解决方案仅比不安全的解决方案慢一点。
鉴于性能损失如此之低,我将选择安全的路线,并将类检查添加到您的contains
方法中。如果按原样保留try-catch解决方案,则可能最终掩盖了keyExtractor.apply
,map.get
,elem.equals
等的实现所引起的将来的错误。