Java for-each цикл throw NullPointException

Следующий сегмент java приведет к исключению NullPointException, поскольку список переменных имеет значение 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 может уменьшить вложенность кода.

Ответы

Ответ 1

Я вижу следующие причины, хотя я понятия не имею, думал ли кто-нибудь об этом, когда он был реализован, и каковы были фактические причины.

  • Как вы показали, текущее поведение цикла for (:) - очень легко понять. Другое поведение не

  • Это было бы единственное, что было бы в Java-сообществе таким образом.

  • Это не будет эквивалентно простому циклу for, поэтому миграция между ними фактически не будет эквивалентной.

  • Использование null - это плохая привычка, так что NPE - отличный способ сообщить разработчику, что "вы F *** разобрались, очистите свой беспорядок" с предлагаемым поведением, проблема будет просто скрыта.

  • Что делать, если вы хотите сделать что-либо еще с массивом до или после цикла... теперь у вас будет двойная проверка в коде.

Ответ 2

Чтобы ответить на ваш первый вопрос: нет, эти три цикла не эквивалентны. Во-вторых, в этих циклах нет нулевой проверки; нет смысла пытаться перебирать то, что не существует.


Предположим, что мы имеем следующий класс:

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: Enhanced For vter Iterator

Здесь байт-код для расширенного цикла.

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. Enhanced For vs. for-Loop

Добавьте в байт-код для цикла 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. Вывод

Существует веская причина, почему список разыменования, который мы разыменовываем, принимается не null - мы вызываем методы, указанные интерфейсом. Если бы эти методы не были реализованы, это было бы иначе: throw a UnsupportedOperationException. Если объект, который мы пытаемся вызвать на контракт, не существует - это просто не имеет смысла.

Ответ 3

Looping null вызовет NullPointerException, поэтому вы всегда должны проверить, является ли список нулевым, вы можете использовать этот общий метод:

public static boolean canLoopList(List<?> list) {
    if (list != null && !list.isEmpty()) {
        return true;
    }
    return false;
}

то перед циклом любого списка проверьте список:

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

Ответ 4

Причина, по которой она не вставляет нулевую проверку, связана с тем, что она не определена. Вы можете найти правила для циклов foreach в разделе 14.14.2 Спецификации языка Java.

Что касается того, почему он спроектирован таким образом, тем больше вопрос, почему бы и нет?

  • Это естественно. цикл foreach ведет себя как эквивалент цикла без магического поведения

  • Желательно. Обычно люди не хотят, чтобы код терпел неудачу, когда возникает ошибка.

Проблема эффективности, предложенная Элвином Вонгом, скорее всего, была незначительной. JVM обычно оптимизирует нулевые проверки в случаях, когда переменная всегда не равна нулю, поэтому влияние производительности незначительно.

Ответ 5

Если у меня есть нулевой массив ArrayList, то сколько объектов оно содержит? Мой ответ равен нулю. Поэтому, по моему мнению, усиленный цикл не должен бросать 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 был брошен или мог обрабатывать нулевые коллекции изящно.

Ответ 6

"Я думаю, что для (Object o: arr) {} эквивалентно

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

Почему ты так думаешь? как может arr.length не выдавать исключение, если arr null? null не может иметь длину.

Если for(Object o :arr) не генерирует исключение, это должно означать, что цикл for (:) достаточно умен, чтобы проверить, является ли arr нулевым, а не пытаться вытащить элементы из него. Очевидно, что цикл for (;;) не такой умный.

Ответ 7

Часто не рекомендуется избегать исключений с нулевым указателем с помощью атрибутов if(o != null) - это может вообще не иметь смысла для o равным нулю, и в этом случае вы хотите бросить и зарегистрировать исключение, если это окажется в этом случае.

Ответ 8

Вы уже ответили на свой вопрос, если arr равно null. arr.lenght выбрасывает NullPointerException. Поэтому for (Object o : arr){ } эквивалентно

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