有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

使用泛型和Varargs的java 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;

我没有将整数数组强制转换回任何地方的对象数组,我也无法真正找出哪里出了问题,因为在这一行中,数组仍然是整数数组,而且它永远不会进入匿名方法

抱歉,如果这是一个我应该很容易找到解决方案的问题,我尝试了,但没有找到。如果你投了反对票,请告诉我原因


共 (1) 个答案

  1. # 1 楼答案

    实际答案

    泛型和数组,以及泛型和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);行抛出异常,但我们甚至从未到达函数调用的主体

    要了解原因,您必须了解数组、泛型和vararg如何一起工作(或不一起工作)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*/}),在调用方法之前在调用方的代码中隐式构建一个数组

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

    泛型和数组

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

    • 如果您有一个Integer[],如果您试图做一些像((Object[]) myIntegerArray)["not a number!"]这样的鬼鬼祟祟的事情,Java会抛出一个运行时错误,否则会使您陷入一种奇怪的情况,整数数组包含一个字符串;要做到这一点,它需要在运行时检查放入数组中的每个值是否与Integer兼容
    • 您可以将Integer[]强制转换到Object[]并返回到Integer[],但不能返回到,比如说,String[],如果您尝试,您将在运行时在向下转换时收到类强制转换异常。Java需要知道该值是作为Integer[]创建的,以支持这一点

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

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

    但是有一个漏洞。如果使用泛型类型调用varargs函数,Java允许程序编译。回想一下,varargs方法的callsite创建了一个新数组,但是它给了该数组什么类型呢?在本例中,它将创建一个Object[],因为Object是对T的擦除

    在某些情况下,这将很好地工作,但您的程序不是其中之一。在您的程序中,func采用的值为

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

    身体并不重要;您可以用return null;替换它,程序的行为将相同。重要的是它需要Integer...,而不是Object...T...之类的东西。记住Integer...是语法糖,编译后确实需要Integer[]。因此,当您对数组调用此方法时,它要做的第一件事是将其强制转换为Integer[]

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

    这个故事的寓意

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

    • 更喜欢像List这样的Java集合而不是数组集合是Java的方式
    • Varargs和泛型不能混合使用当你将它们混合在一起时,你可以很容易地走进像这样的肮脏陷阱。不要对这类事情敞开大门

    希望有帮助