代码之家  ›  专栏  ›  技术社区  ›  Marcel

如何检查泛型方法参数以避免类CastException?

  •  1
  • Marcel  · 技术社区  · 6 年前

    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类型时,我对键不感兴趣,因为在这种情况下,我确信集合不包含该对象。

    但上面提到的是 有几个缺点:

    • 其次,我可能会 类别例外 从内心深处 keyExtractor 不是故意的

    是否可以检查 o 根据参数 键盘提取器 之前 对…的呼唤 .apply 而且没有不合理的运行时开销?


    注意:我知道上面的设计要求E有一个不可变的键。但这没什么大不了的,而且经常发生。

    1 回复  |  直到 6 年前
        1
  •  1
  •   flakes    6 年前

    在擦除之后,ClassCastException怎么会发生?

    应用擦除后,很难确定未检查的强制转换是否会触发 ClassCastException . 毕竟,这些类型被简化为 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 和覆盖 酒吧 如果类型是显式的,则会产生错误。

    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> 合成的 桥梁法 ,以便调用 酒吧 写在 TestInteger . 这种桥接方法是 发生。它看起来像是吼:

    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

    一种方法是注射 等级 isInstance :

    这个方法是Java语言的动态等价物 instanceof 接线员。方法返回 true 对象 参数不为空,可以强制转换为 等级 类别例外 . 它回来了 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 得到一个 实例,但不能保证它的实现也将显式定义类型参数。


    检查实例是否会减慢我的应用程序?

    isInstance公司 实际上相当快。 There's an interesting article 在实验上比较了试捕和不安全抛投的速度 isInstance公司 解决方案。在实验结果中,显式检查类型的解决方案只比不安全的解决方案稍微慢一些。

    contains 方法。如果您保持try-catch解决方案的原样,则可能最终会掩盖由于实现 keyExtractor.apply , map.get , elem.equals