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;
我没有在任何地方将整数数组转换回对象数组,我无法真正弄清楚出了什么问题,因为在该行中,数组仍然是整数数组,它永远不会进入匿名方法。
抱歉,如果这是一个我应该很容易找到解决方案的问题,我试过了,但没有找到。另外,如果您不赞成,请告诉我原因。
解决方案
实用的答案
泛型和数组,以及泛型和可变参数,在 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 中,语法糖意味着两件事:
call
实际上有类型T call(T[] vals)
- 任何调用站点
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[]
因为.Object
T
在某些情况下,这可以正常工作,但您的程序不是其中之一。在您的程序中,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 和泛型不混合。当您将它们混合在一起时,您可以轻松地进入像这样的令人讨厌的陷阱。不要为这些事情开门。
希望有帮助。
推荐阅读
- config - U-Boot defconfig 常用配置
- c# - 队列触发 Azure 函数:队列消息处理多次
- python - 无法在 Jupyter Notebook (Python 3.6) 中导入模块
- python - 我可以设置默认的 alexa 插槽值吗?
- sql - 数据库更改时:代码优先模式,代码从 EDMX 文件生成,用于数据库优先
- php - 使用 bigquery 时出现注释错误
- python - 使用 Pandas 处理订单 json 数据
- google-cloud-platform - 通过 Cloud Source 存储库部署 Google 云功能停止工作
- cassandra - 如何正确选择与索引匹配的分区键
- python - 提取 Python subprocess.CalledProcessError 参数列表