繁体   English   中英

使用泛型和Varargs的ClassCastException

[英]ClassCastException using Generics and Varargs

我最近刚接触Java泛型,并想复制可与Array一起使用的JavaScript映射函数。 现在我无法弄清楚代码中出了什么问题:

public class Test {

public interface Function<T> {
    public T call(T... vals);
}

public <E> E[] map(E[] array, Function<E> func) {
    for (int i = 0; i < array.length; i++) {
        array[i] = func.call(array[i]);    <--- Exception
    }
    return array;
}

public Test() {
    Integer foo[] = {3, 3, 4, 9};

    foo = map(foo, new Function<Integer>() {
        @Override
        public Integer call(Integer... vals) {
            return vals[0] += 2;
        }
    });
    for (Integer l : foo) {
        System.out.println(l);
    }
}

public static void main(String[] args) {
    new Test();
}
}

我在指定的行遇到ClassCastException:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

我没有在任何地方将Integer Array转换回Object Array,也无法真正弄清楚出了什么问题,因为在那一行中,该数组仍然是Integer Array,并且永远不会进入匿名方法。

抱歉,如果这是我应该轻松找到解决方案的问题,我尝试过但没有发现。 另外,如果您不赞成,请告诉我原因。

实际答案

泛型和数组,以及泛型和varargs在Java中混合不好。 无论如何,数组在Java中并不常用。 在Java中,使用List<Integer>比使用Integer[]更常见,这样可以避免您将要听到的所有麻烦。 因此,对于实际程序,只需不要将泛型和数组或泛型和varargs一起使用,就可以了。

请勿阅读其他内容! 只需使用List<T>而不是T[]并且不要将varags与泛型一起使用!

您已被警告。

技术答案

让我们对问题函数进行一些解压缩。 我们可以将其更明确地重写为:

public <E> E[] map(E[] array, Function<E> func) {
  E e = array[0];
  E res = func.call(e);
  array[0] = res;
  return array;
}

如果在调试器中逐步执行此功能,则将看到E res = func.call(e); 引发了异常,但我们甚至从未到达函数调用的主体。

要了解原因,您必须了解数组,泛型和varargs如何一起工作(或不工作)。 call被声明为public T call(T... vals) 在Java中,语法糖意味着两件事:

  1. call实际上具有T call(T[] vals)类型T call(T[] vals)
  2. 任何call(T t1, T t2, /*etc*/)站点call(T t1, T t2, /*etc*/)都应转换为call(new T[]{t1, t2, /*etc*/}) ,在调用之前隐式地在调用者的代码中构建一个数组。方法。

如果不是在call声明中使用像T这样的类型变量,而是使用像Integer类的普通类型或类似的类型,那么故事就此结束了。 但是由于它是一个类型变量,所以我们必须对泛型和数组之间的相互作用有更多的了解。

泛型和数组

Java中的数组包含一些有关它们是什么类型的运行时信息:例如,当您说new Integer[]{7} ,数组对象本身会记住它是作为Integer数组创建的。 这有两个原因,都与转换有关:

  • 如果您具有Integer[] ,则如果尝试执行诸如((Object[]) myIntegerArray)["not a number!"]等偷偷摸摸的事情,Java会抛出运行时错误,否则将使您陷入一种奇怪的情况,即您的整数数组包含一个字符串; 为此,需要在运行时检查您放入数组中的每个值是否与Integer兼容
  • 您可以将Integer[]Object[] ,再转换为Integer[] ,但不能转换为String[] ,如果尝试,则在运行时会在向下转换时得到类转换异常。 Java需要知道该值是作为Integer[]创建的以支持该值。

另一方面,泛型没有运行时组件。 您可能听说过Erase ,这就是指:所有的泛型内容在编译时都会检查,但会从程序中删除,并且完全不会影响运行时。

那么,如果尝试创建某种通用类型的数组(如new T[]{}什么? 它不会编译,因为该数组需要知道运行时的T才能正常工作,但是Java不知道运行时的T是什么。 因此,编译器根本不允许您构建其中之一。

但是存在漏洞。 如果使用泛型类型调用varargs函数,则Java允许程序进行编译。 回想一下varargs方法的callsite创建了一个新数组-那么它赋予该数组什么类型呢? 在这种情况下,由于ObjectT的擦除,因此将其仅创建为Object[]

在某些情况下,这可以正常工作,但是您的程序不是其中之一。 在您的程序中, func承担的价值是

public Integer call(Integer... vals) {
  return vals[0] += 2;
}

身体并不重要; 您可以将其替换为return null; 该程序的行为将相同。 重要的是它需要Integer... ,而不是Object...T...或类似的东西。 请记住, Integer...是语法糖,编译后的确需要Integer[] 因此,当您在数组上调用此方法时,首先要做的是将其Integer[]Integer[]

就是问题所在:在调用站点,我们只知道该函数使用了T... ,所以我们用新创建的Object[]对其进行了编译(该Object[]在运行时恰好用整数填充,但是编译器没有不知道这一点)。 但是被调用方需要一个Integer[] ,并且由于上述原因,您不能将作为new Object[]{}构造的数组向下转换为Integer[] 尝试时,您会收到java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; 这正是您的程序产生的结果。

这个故事的主旨

没有理由尝试了解上面发生错误的所有详细信息。 相反,这是我希望您带走的东西:

  • 将Java集合(例如List于数组。 集合是Java的方式。
  • 可变参数和泛型不能混合使用。 将它们混合在一起时,您可以轻松地走入此类陷阱。 不要打开这些门。

希望能有所帮助。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM