Java中foreach循环中的ClassCastException

6
在以下代码中,什么情况下会出现ClassCastException:
import java.util.Arrays;
import java.util.List;

public class Generics {

    static List getObjects() {
        return Arrays.asList(1, 2, 3);
    }

    public static void main(String[] args) {
        List<String> list = getObjects();
        for (Object o : list) { // ClassCastException?
            System.out.println(o);
        }
    }
}

我们在生产环境中遇到了一个类似的情况(我知道这是不好的实践),客户提供了一个带有ClassCastException的日志,指向了带有注释的代码行,但我似乎无法复现它。有什么想法吗?
我知道当使用foreach时JVM会在后台创建一个迭代器,但它是否可以在某些情况下创建一个原始的迭代器,在其他情况下创建一个参数化的迭代器?
更新:我还查看了生成的字节码,在Windows上,使用JDK 1.6.0_21-b07没有进行checkcast。有趣 :)
这是主要方法: public static void main(java.lang.String[]); Code: 0: 调用静态方法 #34; //Method getObjects:()Ljava/util/List; 3: 存储结果到本地变量1 4: 加载本地变量1 5: 调用接口方法 #36, 1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 10: 存储结果到本地变量3 11: 跳转到 28 14: 加载本地变量3 15: 调用接口方法 #42, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 20: 存储结果到本地变量2 21: 获取静态字段 #48; //Field java/lang/System.out:Ljava/io/PrintStream; 24: 加载本地变量2 25: 调用实例方法 #54; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 28: 加载本地变量3 29: 调用接口方法 #60, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z 34: 如果为真跳转到 14 37: 返回 更新2: 我被使用其自己的编译器的Eclipse IDE所误导,因此上面的字节码实际上是使用Eclipse编译器生成的。请看这里如何手动使用Eclipse编译代码。 总之,在某些情况下,Eclipse编译器会生成不同于Sun编译器的字节码,无论平台如何,本文描述的情况就是其中之一。

Java泛型正在咬你!;-) - Lucero
2
提示:(1)利用所有可用的信息 - 异常应该告诉你涉及哪些类,例如 java.lang.ClassCastException: XXXX cannot be cast to YYYY。 (2)行号有时会偏差几行,因此请查看上下报告行数的几行,以寻找异常来源可能性。 - Bert F
确实,只返回精确的翻译信息。 - Bozho
上面的代码是生产代码的模仿,但“翻译”的错误是ClassCastException Integer无法转换为String。 - user174852
感谢您发布您的反汇编内容。关于checkcast在某些已编译版本(例如我的版本和Tim在使用1.6.0_21时的版本)中存在,而在其他版本(例如bozho和您的版本)中不存在,这真是令人着迷。 - T.J. Crowder
4个回答

5
这段代码难道不应该总是抛出ClassCastException吗?在我使用Sun Java 6编译器和运行时(在Linux上)时,它会抛出异常。您正在将Integer转换为String。创建的迭代器将是一个Iterator<String>,但然后它试图访问第一个元素,即一个Integer,因此失败。
如果您像这样更改数组,则情况会变得更清晰:
return Arrays.asList("one", 2, 3);

现在循环实际上对于第一个元素起作用,因为第一个元素是一个 String 并且我们看到了输出;然后 Iterator<String> 在第二个元素时失败了,因为它不是一个字符串。

如果你只使用一个通用的 List 而不是特定的列表,则您的代码将起作用:

List list = getObjects();
for (Object o : list) {
    System.out.println(o);
}

如果您使用的是List<Integer>,因为内容是Integer,那么您当然可以这样做。您现在所做的会触发编译器警告,原因很明显:注意:Generics.java使用未经检查或不安全的操作。

以下修改也有效:

for (Object o : (List)list)

......这可能是因为此时您正在处理的是一个Iterator,而不是Iterator<String>

bozho说他在Windows XP上没有看到这个错误(没有提及使用哪个编译器和运行时,但我猜测是Sun的),而您说您没有看到它(或者不太可靠),所以显然这里存在一些实现上的敏感性,但底线是:不要使用List<String>List中的Integer交互。 :-)

这是我正在编译的文件:

import java.util.Arrays;
import java.util.List;

public class Generics {

    static List getObjects() {
        return Arrays.asList("one", 2, 3);
    }

    public static void main(String[] args) {
        List<String> list = getObjects();
        for (Object o : list) { // ClassCastException?
            System.out.println(o);
        }
    }
}

以下是编译结果:

tjc@forge:~/temp$ javac Generics.java 
注意:Generics.java使用了未经检查或不安全的操作。
注意:请使用-Xlint:unchecked重新编译以获取详细信息。

以下是运行结果:

tjc@forge:~/temp$ java Generics 
one
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at Generics.main(Generics.java:12)

第12行是for语句。请注意,它输出了第一个元素,因为我将其更改为了String。它没有输出其他元素。(在我做出这个更改之前,它立即失败了。)

我正在使用的编译器如下:

tjc@forge:~/temp$ which javac
/usr/bin/javac
tjc@forge:~/temp$ ll /usr/bin/javac
lrwxrwxrwx 1 root root 23 2010-09-30 16:37 /usr/bin/javac -> /etc/alternatives/javac*
tjc@forge:~/temp$ ll /etc/alternatives/javac
lrwxrwxrwx 1 root root 33 2010-09-30 16:37 /etc/alternatives/javac -> /usr/lib/jvm/java-6-sun/bin/javac*

以下是反汇编代码,其中显示了checkcast指令:

tjc@forge:~/temp$ javap -c Generics
从 "Generics.java" 编译而来
public class Generics extends java.lang.Object{
public Generics();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."":()V
   4:   return
static java.util.List getObjects(); Code: 0: iconst_3 1: anewarray #2; //class java/io/Serializable 4: dup 5: iconst_0 6: ldc #3; //String one 8: aastore 9: dup 10: iconst_1 11: iconst_2 12: invokestatic #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 15: aastore 16: dup 17: iconst_2 18: iconst_3 19: invokestatic #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 22: aastore 23: invokestatic #5; //Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List; 26: areturn public static void main(java.lang.String[]); Code: 0: 调用静态方法 #6; //Method getObjects:()Ljava/util/List; 3: astore_1 4: aload_1 5: 调用接口方法 #7, 1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 10: astore_2 11: aload_2 12: 调用接口方法 #8, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z 17: 如果为假跳转到40行 20: aload_2 21: 调用接口方法 #9, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 26: 强制类型转换为 #10; //class java/lang/String 29: astore_3 30: 获取静态字段 #11; //Field java/lang/System.out:Ljava/io/PrintStream; 33: aload_3 34: 调用虚方法 #12; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 37: 跳转到11行 40: return
}

总之,不要使用 List<String> 与包含非 StringList 进行交互。:-)


1
字符串部分在运行时丢失(擦除),因此实际上没有进行任何转换。但按照他的方式这样做非常错误。 - Bozho
2
我运行了它,没有抛出异常:),1.6.0_20,Windows XP - Bozho
1
@Bozho:嗯......在1.6.0_21上编译(对我而言)会插入一个“checkcast <java/lang/String>”指令,这会导致异常。 - Tim Stone
3
这是一个相当有趣的讨论,但我认为我们都同意代码存在的问题以及需要修复的地方 :) - Bozho
1
@Bozho: “...但我认为我们都同意代码存在的问题以及需要修复的地方。” 完全同意 :-) - T.J. Crowder
显示剩余9条评论

1

我也无法重现它,但我发现以下必须更正的错误:

  • 使getObjects()返回List<Integer>而不是原始类型
  • 不要期望List<String>,而是期望List<Integer>
  • 在迭代时,循环使用for (Integer o : list)

0

这部分:

List<String> list = getObjects();
for (Object o : list) { // ClassCastException?
    System.out.println(o);
}

将被简化为

List<String> list = getObjects();
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    Object o = iterator.next();
    System.out.println(o);
}

但是,迭代器的实现在调用next()方法时,会尝试将Iterator发送的内容强制转换为String

这就是为什么会出现CCE的原因。

解决方案:

要么在任何地方都使用泛型,要么不使用它们,但保持一致非常重要。如果您返回了List<Integer>List<? super Integer>,则会在编译时看到此问题。


我认为你想表达的是 Iterator<String>,而不是 Iterator<List> - T.J. Crowder

0
问题在于方法 static List getObjects() { 返回一个通用(非参数化)的 List。而你将其赋值给了 List<String>。这一行应该会产生编译器警告,这应该是一个问题的信号。

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