如何检查通用方法参数以避免ClassCastException?

时间:2018-11-25 09:53:32

标签: java generics classcastexception

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具有不可变的密钥。但这没什么大不了,而且经常发生。

1 个答案:

答案 0 :(得分:1)

擦除后如何发生ClassCastException?

应用删除后,很难确定未经检查的强制转换是否会触发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>


检查instanceof会降低我的应用程序速度吗?

isInstance的执行时间实际上非常快。 There's an interesting article实验性地比较了使用不安全强制转换的尝试捕获速度与isInstance解决方案。在实验结果中,显式检查类型的解决方案仅比不安全的解决方案慢一点。

鉴于性能损失如此之低,我将选择安全的路线,并将类检查添加到您的contains方法中。如果按原样保留try-catch解决方案,则可能最终掩盖了keyExtractor.applymap.getelem.equals等的实现所引起的将来的错误。