Java 的 for-each 循环抛出 NullPointException。

27
以下 Java 代码段将抛出 NullPointException,因为变量 list 是 null,而这个 null 值被传递给 for-each 循环。
List<> arr = null;
for (Object o : arr) {
    System.out.println("ln "+o);
}

我认为 for (Object o : arr){ }

for (int i = 0; i < arr.length; i++) { } 是等价的

以及/或者

for (Iterator<type> iter = arr.iterator(); iter.hasNext(); ){ 
   type var = iter.next(); 
}

无论哪种情况,若arr为null,则会导致arr.length或arr.iterator()抛出NullPointException异常

我只是好奇为什么for (Object o : arr){ }不被翻译成

if (arr!=null){
  for (int i = 0; i < arr.length; i++) { 
  }
}
and
if (arr!=null){
    for (Iterator<type> iter = arr.iterator(); iter.hasNext(); ){ 
       type var = iter.next(); 
    }
}

包含 arr!=null 表达式可以减少代码嵌套。


如果您已经知道arr不是null,您是否希望它再进行一次无用的null检查呢?我想答案是否定的。 - Alvin Wong
1
我看不出有什么理由想要迭代一个已知为空的列表。为什么空检查应该在增强型for循环中,而不是在迭代之前列表就永远不为空呢? - Makoto
我认为“已知空列表”只是为了解释问题的行为。 - nclord
8个回答

23

我看到以下几个原因,虽然我不知道是否有人在实现的时候考虑过这些问题,以及实际的原因是什么。

  1. 正如你所演示的那样,for(:)循环的当前行为非常易于理解。另一种行为则不然。

  2. 它将是java世界中唯一以这种方式行事的东西。

  3. 它将不等同于简单的for循环,因此在两者之间迁移实际上并不等效。

  4. 使用null本来就是一种不良习惯,因此NPE是通知开发人员“你已经搞砸了,请清理你的代码”的好方法。采用建议的行为,问题只会被隐藏。

  5. 如果您想在循环之前或之后对数组进行任何其他操作...现在您的代码中需要两次进行null检查。


12

回答你的第一个问题:不,这三个循环不等价。其次,在这些循环中找不到空值检查;试图遍历不存在的内容是没有意义的。


假设我们有以下类:

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

public class EnhancedFor {


    private List<Integer> dummyList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
    private List<Integer> nullList = null;

    public void enhancedForDummyList() {
        for(Integer i : dummyList) {
            System.out.println(i);
        }
    }

    public void iteratorDummyList() {
        for(Iterator<Integer> iterator = dummyList.iterator(); iterator.hasNext();) {
            System.out.println(iterator.next());
        }
    }

    public void normalLoopDummyList() {
        for(int i = 0; i < dummyList.size(); i++) {
            System.out.println(dummyList.get(i));
        }
    }
}

我们将对其进行字节码分解,看看这些循环之间是否有任何区别。

1:增强型for循环 vs. 迭代器

这是增强型for循环的字节码。

public enhancedForDummyList()V
   L0
    LINENUMBER 12 L0
    ALOAD 0
    GETFIELD EnhancedFor.dummyList : Ljava/util/List;
    INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
    ASTORE 1
   L1
   FRAME APPEND [java/util/Iterator]
    ALOAD 1
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z
    IFEQ L2
    ALOAD 1
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
    CHECKCAST java/lang/Integer
    ASTORE 2
   L3
    LINENUMBER 13 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 2
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L4
    LINENUMBER 14 L4
    GOTO L1
   L2
    LINENUMBER 15 L2
   FRAME CHOP 1
    RETURN
   L5
    LOCALVARIABLE i Ljava/lang/Integer; L3 L4 2
    LOCALVARIABLE i$ Ljava/util/Iterator; L1 L2 1
    LOCALVARIABLE this LEnhancedFor; L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 3

以下是迭代器的字节码。

public iteratorDummyList()V
   L0
    LINENUMBER 24 L0
    ALOAD 0
    GETFIELD EnhancedFor.dummyList : Ljava/util/List;
    INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
    ASTORE 1
   L1
   FRAME APPEND [java/util/Iterator]
    ALOAD 1
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z
    IFEQ L2
   L3
    LINENUMBER 25 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
    GOTO L1
   L2
    LINENUMBER 27 L2
   FRAME CHOP 1
    RETURN
   L4
    LOCALVARIABLE iterator Ljava/util/Iterator; L1 L2 1
    // signature Ljava/util/Iterator<Ljava/lang/Integer;>;
    // declaration: java.util.Iterator<java.lang.Integer>
    LOCALVARIABLE this LEnhancedFor; L0 L4 0
    MAXSTACK = 2
    MAXLOCALS = 2

总的来说,它看起来像他们在做非常相似的事情。 他们使用相同的接口。不同之处在于增强型for循环使用当前值(i)和指向列表其余部分的光标(i$)这两个变量,而迭代器只需要光标来调用.next()

类似,但不完全相同。

2. 增强型for循环 vs for循环

让我们添加for循环的字节码。

public normalLoopDummyList()V
   L0
    LINENUMBER 24 L0
    ICONST_0
    ISTORE 1
   L1
   FRAME APPEND [I]
    ILOAD 1
    ALOAD 0
    GETFIELD EnhancedFor.dummyList : Ljava/util/List;
    INVOKEINTERFACE java/util/List.size ()I
    IF_ICMPGE L2
   L3
    LINENUMBER 25 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    GETFIELD EnhancedFor.dummyList : Ljava/util/List;
    ILOAD 1
    INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L4
    LINENUMBER 24 L4
    IINC 1 1
    GOTO L1
   L2
    LINENUMBER 27 L2
   FRAME CHOP 1
    RETURN
   L5
    LOCALVARIABLE i I L1 L2 1
    LOCALVARIABLE this LEnhancedFor; L0 L5 0
    MAXSTACK = 3
    MAXLOCALS = 2

它正在做一些不同的事情。 它根本没有使用Iterator接口。 相反,我们在调用get(),这个方法只由List指定,而不是Iterator

3. 结论

有一个有效的理由解释为什么我们要假设我们正在取消引用的列表不为空 - 我们正在调用由接口指定的方法。 如果那些方法未被实现,那将是不同的情况:抛出UnsupportedOperationException。如果我们试图在不存在的对象上调用契约,那就毫无意义了。


5
循环空值将导致NullPointerException,因此您必须始终检查列表是否为空,您可以使用以下通用方法:
public static boolean canLoopList(List<?> list) {
    if (list != null && !list.isEmpty()) {
        return true;
    }
    return false;
}

在循环任何列表之前,请检查该列表:

if (canLoopList(yourList)) {
    for(Type var : yourList) {
    ...
}
}

2
它没有插入null检查的原因是因为它没有被定义。您可以在Java语言规范的第14.14.2节中找到foreach循环的规则。
至于为什么设计成这样,更大的问题是为什么不呢?
- 它很自然。foreach循环的行为就像一个等价的for循环,没有任何神奇的行为。 - 它是期望的。当出现错误时,人们通常不希望代码静默地失败。
Alvin Wong提出的性能问题可能是最好的次要考虑因素。JVM通常会在变量始终为非空时优化掉null检查,因此性能影响可以忽略不计。

1
您已经回答了自己的问题,如果arr为空,则arr.length会引发NullPointerException。因此,for (Object o : arr){ }等同于。
for (int i = 0; i < arr.length; i++) { } 

0
如果我有一个空的 ArrayList,那么它包含多少个对象?我的答案是零。所以在我的想法中,增强型 for 循环不应该抛出 NPE。
List<Object> myList = null;
for (Object obj : myList) {
    System.out.println(obj.toString());
}

但事实就是这样。显然,现在 Java 规范不会改变,所以也许他们应该引入 Elvis 和安全导航运算符来支持这一点:

List<Object> myList = null;
for (Object obj ?: myList) {
    System.out.println(obj?.toString());
}

那么开发人员可以选择是让 NPE 抛出还是优雅地处理空集合。


如果我有一个“null”“ArrayList”...是无意义的。你不能拥有一个“null”任何东西。“null”意味着你_没有_它。如果没有“它”,询问“它”包含多少对象就没有意义。另一方面,如果你有一个_空_的ArrayList,那么它包含零个对象。 - Erick G. Hagstrom
提议的语法很有道理;在迭代字段之前不必检查其存在性通常是非常有用的,特别是对于第三方库而言,在那里你无法控制实例化列表。例如,在javax.mail.MimeMessage中循环遍历CC收件人。 - nclord

0

我认为 for(Object o : arr){ } 相当于

for (int i = 0; i < arr.length; i++) { }

你为什么这样认为?如果 arr 是 null,怎么可能不抛出异常呢?null 没有长度。

如果 for(Object o :arr) 不会抛出异常,那一定是 for(:) 循环足够聪明,能够检查 arr 是否为 null,并且不尝试从中提取项目。显然,for(;;) 循环没有那么聪明。


使用 for(Object o :arr) 检查 arr 是否为 null 不是一个明智的做法。 - Linlin
1
如果它不抛出异常,那就一定是! :) - Yevgeny Simkin

0
通常使用if(o != null)保护来避免空指针异常并不是一个好主意 - 如果o为空根本没有意义,那么如果情况确实如此,您希望抛出并记录异常。

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