使用Generics和Varargs的ClassCastException

时间:2018-05-14 14:46:15

标签: java arrays generics

我最近刚进入Java Generics并希望复制可以与Arrays一起使用的JavaScript map函数。现在我无法弄清楚我的代码中出了什么问题:

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数组转换回任何地方的Object Array,我无法弄清楚出现了什么问题,因为在该行中数组仍然是一个整数数组,它永远不会进入匿名方法。

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

1 个答案:

答案 0 :(得分:3)

实际答案

泛型和数组,以及泛型和变量,在Java中不能很好地混合。无论如何,Java中的数组使用不多;在Java中,使用List<Integer>而不是Integer[]更为常见,这样做可以避免您将要听到的所有痛苦。因此对于实际程序,只是不要将泛型和数组,或泛型和变量一起使用,你会没事的。

请勿阅读此内容!只需使用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);抛出了异常,但我们甚至都没有进入函数调用的主体。

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

  1. call实际上具有T call(T[] vals)
  2. 类型
  3. 任何调用点call(T t1, T t2, /*etc*/)都应转换为call(new T[]{t1, t2, /*etc*/}),在调用方法之前隐式地在调用者的代码中构建数组。
  4. 如果不是在T的声明中使用类似call的类型变量,而是使用像Integer这样的普通类型,那么这将是结束故事。但由于它是一个类型变量,我们必须更多地了解泛型和数组之间的相互作用。

    泛型和数组

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

    • 如果您有Integer[],如果您尝试像((Object[]) myIntegerArray)["not a number!"]那样偷偷摸摸地执行某些操作,那么Java会抛出运行时错误,否则会让您陷入奇怪的情况,即您的整数数组包含字符串;要做到这一点,它需要在运行时检查您放入数组中的每个值是否与Integer
    • 兼容
    • 您可以将Integer[]投降至Object[]并返回Integer[],但不要退回至String[] - 如果您尝试将获得运行时向下转换的类转换异常。 Java需要知道该值是作为Integer[]创建的,以支持它。

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

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

    但是存在漏洞。如果使用泛型类型调用varargs函数,Java允许程序进行编译。回想一下,varargs方法的调用点创建了一个新数组 - 所以它给那个数组提供了什么类型?在这种情况下,它只会将其创建为Object[],因为ObjectT的删除。

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

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

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

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

    故事的寓意

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

    • 首选Java集合,例如List到数组。集合是Java方式。
    • Varargs和generics不会混合。当你把它们混合在一起时,你可以很容易地走进像这样的讨厌的陷阱。不要打开这些东西的大门。

    希望有所帮助。