【问题标题】:ClassCastException using Generics and VarargsClassCastException 使用泛型和可变参数
【发布时间】:2018-05-14 14:46:14
【问题描述】:

我最近才接触到 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;

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

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

【问题讨论】:

  • 为什么需要T... vals
  • 因为我希望能够多用途地使用它
  • 简短回答,你不能创建通用数组,这就是为什么编译器创建一个 Object[] 并尝试将其转换为 Integer[] 但数组不是多态的,因此 ClassCastException
  • 你应该收到警告

标签: java arrays generics


【解决方案1】:

实用答案

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

别再读了!只需使用 List&lt;T&gt; 而不是 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*/}),在调用方法之前在调用者的代码中隐式构建一个数组。

如果不是在call 的声明中使用像T 这样的类型变量,而是使用像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 在运行时是什么。所以编译器根本不允许你构建其中之一。

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

在某些情况下,这可以正常工作,但您的程序不是其中之一。在你的程序中,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;,这正是你的程序产生的。

故事的寓意

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

  • 比起数组更喜欢 Java 集合,例如 List 集合是 Java 的方式。
  • Varargs 和泛型不能混用。当你把它们混在一起时,你很容易陷入像这样的讨厌的陷阱。不要为这类事情敞开大门。

希望对您有所帮助。

【讨论】:

  • “更喜欢 Java 集合,例如 List,而不是数组。”但这与这里的问题并不真正相关。他们可以摆脱E[] 并更改map 函数以将E 的单个值映射到E 的单个值,并且会遇到同样的问题。问题在于可变参数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多