使用可变参数和泛型时出现ClassCastException异常

10
我正在使用java的泛型和varargs。
如果我使用以下代码,则会出现ClassCastException,即使我根本没有使用强制转换。
更奇怪的是,如果在Android(dalvik)上运行此代码,则异常中不包括堆栈跟踪,如果将接口更改为抽象类,则异常变量“e”为空。
代码:
public class GenericsTest {
    public class Task<T> {
        public void doStuff(T param, Callback<T> callback) {
            // This gets called, param is String "importantStuff"

            // Working workaround:
            //T[] arr = (T[]) Array.newInstance(param.getClass(), 1);
            //arr[0] = param;
            //callback.stuffDone(arr);

            // WARNING: Type safety: A generic array of T is created for a varargs parameter
            callback.stuffDone(param);
        }
    }

    public interface Callback<T> {
        // WARNING: Type safety: Potential heap pollution via varargs parameter params
        public void stuffDone(T... params);
    }

    public void run() {
        Task<String> task = new Task<String>();
        try {
            task.doStuff("importantStuff", new Callback<String>() {
                public void stuffDone(String... params) {
                    // This never gets called
                    System.out.println(params);
                }});
        } catch (ClassCastException e) {
            // e contains "java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;"
            System.out.println(e.toString());
        }
    }

    public static void main(String[] args) {
        new GenericsTest().run();
    }
}

如果运行这段代码,你会遇到一个ClassCastException错误,提示无法将Object转换为String,并且堆栈跟踪指向无效的行号。这是Java的一个bug吗?我已经在Java 7和Android API 8中进行了测试。我对此进行了处理(在doStuff方法中用注释掉的方式),但是这么做似乎有点愚蠢。如果我去掉可变参数(T...),一切都能正常工作,但我的实际实现需要它。

异常的堆栈跟踪如下:

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at GenericsTest$1.stuffDone(GenericsTest.java:1)
    at GenericsTest$Task.doStuff(GenericsTest.java:14)
    at GenericsTest.run(GenericsTest.java:26)
    at GenericsTest.main(GenericsTest.java:39)

1
你能提供一份堆栈跟踪的副本吗?我怀疑这是由于类型擦除导致的隐式转换。 - Matt
将堆栈跟踪添加到问题。 - murgo
3个回答

14

这是预期的行为。当您在Java中使用泛型时,编译后的字节码中不包括对象的实际类型(这被称为类型擦除)。所有类型都变成了Object,并且强制转换插入到编译代码中以模拟类型行为。

此外,可变长参数变成数组,当调用泛型可变长参数方法时,Java会创建一个Object[]类型的数组,该数组包含调用前的方法参数。

因此,您的行callback.stuffDone(param);编译为callback.stuffDone(new Object[] { param });。然而,您的回调实现需要一个String[]类型的数组。Java编译器在您的代码中插入了一个隐藏的强制类型转换来强制执行此类型,并且由于Object[]无法转换为String[],因此会出现异常。您看到的虚假行号可能是因为强制类型转换在代码中没有出现。

解决此问题的一种方法是完全从Callback接口和类中删除泛型,将所有类型替换为Object


好的,我明白了。只是奇怪的是它被编译成了 new Object[] { param } 而不是 new String[] { param },后者会起作用。编译器有所有信息来进行更好的转换(它知道无论如何都要将其转换为 String[])。这和堆栈跟踪中的错误行号让我认为这是 Java 的一个 bug。在我的实际程序中删除了可变参数,保留了泛型。 - murgo
@murgo:“编译器拥有所有信息...”并不是。数组是在调用varargs函数时创建的,即在doStuff中,当您调用callback.stuffDone(param);时。在那个地方,无论是在编译时还是运行时,它都不知道T会是什么。 - newacct

1

grahamparks的回答是正确的。神秘的类型转换是正常行为。编译器插入它们是为了确保应用程序在可能出现泛型不正确使用的情况下运行时类型安全。

如果您遵循规则,此类型转换将始终成功。它失败是因为您忽略/抑制了有关不安全使用泛型的警告。这不是明智的做法...特别是如果您不完全理解它们的含义以及是否可以安全地忽略。


在问题中显示了警告,但我仍然觉得有些可疑,因为我认为我应该以“工作方式”使用泛型。这应该是编译错误而不是警告。 - murgo
1
@murgo - 这些是警告,因为有些情况下忽略它们是安全的。实际上,有些情况下最好的解决方案就是忽略/抑制警告。 - Stephen C

1

这确实是由于类型擦除造成的,但关键部分在于可变参数。正如已经注意到的那样,它们被实现为表格。因此,编译器实际上正在创建一个Object[]来打包您的参数,因此稍后会出现无效的转换。 但是有一个解决方法:如果您足够友好地将表格作为可变参数传递,编译器将识别它,不会重新打包它,并且因为您为它节省了一些工作,它将让您运行代码 :-)

尝试按照以下修改运行:

public void doStuff(T[] param, Callback callback) {

task.doStuff(new String[]{"importantStuff"}, new Callback() {


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接