首页 > 解决方案 > ClassCastException 使用泛型和可变参数

问题描述

我最近接触了 Java 泛型,想复制可以与数组一起使用的 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;

我没有在任何地方将整数数组转换回对象数组,我无法真正弄清楚出了什么问题,因为在该行中,数组仍然是整数数组,它永远不会进入匿名方法。

抱歉,如果这是一个我应该很容易找到解决方案的问题,我试过了,但没有找到。另外,如果您不赞成,请告诉我原因。

标签: javaarraysgenerics

解决方案


实用的答案

泛型和数组,以及泛型和可变参数,在 Java 中不能很好地混合。无论如何,数组在 Java 中的使用并不多。List<Integer>在 Java 中,使用 a比使用 an更常见Integer[],这样做可以避免您即将听到的所有痛苦。所以对于实际的程序,不要同时使用泛型和数组,或者泛型和可变参数,你会没事的。

不要阅读比这更进一步的内容!只需使用List<T>T[]不是使用带有泛型的可变参数!

你已经被警告过了。

技术答案

让我们稍微解开问题函数。我们可以更明确地将其重写为:

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);正在引发异常,但我们甚至从未到达函数调用的主体。

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

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

如果不是T在声明中使用类型变量,call而是使用普通类型Integer或类似的东西,这将是故事的结尾。但是由于它是一个类型变量,我们必须更多地了解泛型和数组之间的相互作用。

泛型和数组

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

  • 如果你有一个Integer[],如果你试图做一些鬼鬼祟祟的事情,Java 会抛出一个运行时错误((Object[]) myIntegerArray)["not a number!"],否则会让你陷入一个奇怪的情况,即你的整数数组包含一个字符串;为此,它需要在运行时检查您放入数组中的每个值是否兼容Integer
  • 您可以Integer[]向上转换Object[]和向下转换为Integer[],但不能回退到,例如,String[]如果您尝试,您将在运行时在向下转换时获得类转换异常。Java 需要知道该值是作为Integer[]支持而创建的。

另一方面,泛型没有运行时组件。您可能听说过erasure,这就是它所指的:所有泛型的东西都在编译时检查,但从程序中删除,根本不影响运行时。

那么如果你尝试创建一个泛型类型的数组会发生什么,比如new T[]{}?它不会编译,因为数组需要知道T运行时是什么才能工作,但 Java 不知道运行时是什么T。所以编译器根本不允许你构建其中之一。

但是有一个漏洞。如果调用泛型类型的可变参数函数,Java 允许程序编译。回想一下,可变参数方法的调用点创建了一个新数组——那么它赋予该数组什么类型?在这种情况下,它只会将其创建为,Object[]因为.ObjectT

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

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

身体不重要;您可以将其替换为,return null;程序的行为将相同。重要的是它需要Integer..., not Object...orT...或类似的东西。请记住,这Integer...是语法糖,编译后确实需要Integer[]. 因此,当您在数组上调用此方法时,它要做的第一件事就是将其转换为Integer[].

这就是问题所在:在调用点,我们只知道这个函数取了T...,所以我们用新创建的Object[](恰好在运行时用整数填充,但编译器不知道静态)编译它。但是被调用者需要 a ,并且由于上述原因Integer[],您不能将构造为 a 的数组向下转换new Object[]{}Integer[]当你尝试时,你得到java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;的正是你的程序产生的。

这个故事的主旨

没有理由尝试了解上述问题的所有细节。相反,这是我希望你带走的:

  • 更喜欢 Java 集合,例如List数组。集合是 Java 方式。
  • Varargs 和泛型不混合。当您将它们混合在一起时,您可以轻松地进入像这样的令人讨厌的陷阱。不要为这些事情开门。

希望有帮助。


推荐阅读