Что является более эффективным: цикл для каждого или итератор?

208

Какой самый эффективный способ пройти через коллекцию?

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a) {
  integer.toString();
}

или

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();) {
   Integer integer = (Integer) iterator.next();
   integer.toString();
}

Обратите внимание, что это не является точной копией этого , этого , этого или этого , хотя один из ответов на последний вопрос близок. Причина, по которой это не обман, заключается в том, что большинство из них сравнивают циклы, когда вы вызываете их get(i)внутри цикла, а не используете итератор.

Как предложено на Meta, я буду публиковать свой ответ на этот вопрос.

Пол Уогланд
источник
Я бы подумал, что это не имеет значения, поскольку его Java и механизм шаблонов - это чуть больше, чем синтаксический сахар
Хасан Сайед
Потенциальный дубликат: stackoverflow.com/questions/89891/…
OMG Ponies
2
@OMG Ponies: я не считаю, что это дубликат, поскольку он не сравнивает цикл с итератором, а скорее спрашивает, почему коллекции возвращают итераторы, а не имеют итераторы непосредственно в классе.
Пол Уогланд

Ответы:

266

Если вы просто бродите по коллекции, чтобы прочитать все значения, то нет разницы между использованием итератора или нового синтаксиса цикла for, поскольку новый синтаксис просто использует итератор под водой.

Однако, если вы подразумеваете под циклом старый цикл "c-style":

for(int i=0; i<list.size(); i++) {
   Object o = list.get(i);
}

Тогда новый цикл for или итератор может быть намного более эффективным, в зависимости от базовой структуры данных. Причина этого заключается в том, что для некоторых структур данных get(i)это операция O (n), которая делает цикл операцией O (n 2 ). Традиционный связанный список является примером такой структуры данных. Все итераторы имеют в качестве основного требования, что это next()должна быть операция O (1), составляющая цикл O (n).

Чтобы убедиться, что итератор используется под водой новым синтаксисом цикла for, сравните сгенерированные байт-коды из следующих двух фрагментов Java. Первый цикл for:

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a)
{
  integer.toString();
}
// Byte code
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 3
 GOTO L2
L3
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 2 
 ALOAD 2
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L2
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L3

И второе, итератор:

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();)
{
  Integer integer = (Integer) iterator.next();
  integer.toString();
}
// Bytecode:
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 2
 GOTO L7
L8
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 3
 ALOAD 3
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L7
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L8

Как видите, сгенерированный байт-код фактически идентичен, поэтому использование любой формы не снижает производительности. Поэтому вы должны выбрать форму цикла, которая наиболее эстетически привлекательна для вас, для большинства людей это будет цикл for-each, поскольку в нем меньше стандартного кода.

Пол Уогланд
источник
4
Я считаю, что он говорил обратное: foo.get (i) может быть намного менее эффективным. Подумайте о LinkedList. Если вы делаете foo.get (i) в середине LinkedList, он должен пройти через все предыдущие узлы, чтобы добраться до i. Итератор, с другой стороны, будет держать указатель на базовую структуру данных и позволит вам обходить узлы по одному за раз.
Майкл Крауклис
1
Не большая вещь, но for(int i; i < list.size(); i++) {цикл стиля также должен вычисляться list.size()в конце каждой итерации, если он используется, иногда более эффективно кешировать результат list.size()first.
Бретт Райан
3
Фактически, исходное утверждение также верно для случая ArrayList и всех других, которые реализуют интерфейс RandomAccess. Цикл в стиле C быстрее, чем цикл на основе итераторов. docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp
4
Одной из причин использования старого цикла в стиле C, а не подхода итератора, независимо от того, является ли это foreach или версией desugar, является мусор. Многие структуры данных создают новый итератор при вызове .iterator (), однако к ним можно обращаться без выделения ресурсов с помощью цикла в стиле C. Это может быть важно в определенных высокопроизводительных средах, где стараются избегать (а) попадания в распределитель или (б) сборки мусора.
Дэн
3
Так же, как и другой комментарий, для ArrayLists цикл for (int i = 0 ....) примерно в 2 раза быстрее, чем использование итератора или подхода for (:), поэтому он действительно зависит от базовой структуры. И, как примечание, итерации HashSets также очень дороги (намного больше, чем Array List), поэтому избегайте таких, как чума (если можете).
Лев
106

Разница не в производительности, а в возможностях. При непосредственном использовании ссылки у вас есть больше возможностей явно использовать тип итератора (например, List.iterator () и List.listIterator (), хотя в большинстве случаев они возвращают одну и ту же реализацию). У вас также есть возможность ссылаться на Итератор в вашем цикле. Это позволяет вам делать такие вещи, как удаление элементов из вашей коллекции без получения исключения ConcurrentModificationException.

например

Хорошо:

Set<Object> set = new HashSet<Object>();
// add some items to the set

Iterator<Object> setIterator = set.iterator();
while(setIterator.hasNext()){
     Object o = setIterator.next();
     if(o meets some condition){
          setIterator.remove();
     }
}

Это не так, поскольку это вызовет исключение одновременной модификации:

Set<Object> set = new HashSet<Object>();
// add some items to the set

for(Object o : set){
     if(o meets some condition){
          set.remove(o);
     }
}
Майкл Крауклис
источник
12
Это очень верно, хотя и не дает прямого ответа на поставленный мною вопрос: +1 за информативность и ответ на логический вопрос.
Пол Уогланд
1
Да, мы можем получить доступ к элементам коллекции с помощью цикла foreach, но мы не можем удалить их, но мы можем удалить элементы с помощью Iterator.
Akash5288
22

Чтобы расширить собственный ответ Пола, он продемонстрировал, что байт-код одинаков на этом конкретном компиляторе (предположительно, в javac от Sun?), Но разные компиляторы не гарантируют генерацию одного и того же байт-кода, верно? Чтобы увидеть разницу между ними, давайте сразу перейдем к источнику и проверим Спецификацию языка Java, в частности 14.14.2, «Улучшенный оператор for» :

Расширенный forоператор эквивалентен базовому forутверждению в форме:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    VariableModifiers(opt) Type Identifier = #i.next();    
    Statement 
}

Другими словами, JLS требует, чтобы эти два были эквивалентны. Теоретически это может означать незначительные различия в байт-коде, но в действительности расширенный цикл for необходим для:

  • Вызвать .iterator()метод
  • использование .hasNext()
  • Сделать локальную переменную доступной через .next()

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

Коуэн
источник
На самом деле, тест, который я провел, был с компилятором Eclipse, но ваша общая точка зрения остается в силе. +1
Пол Уогланд
3

foreachПодкапотное создающемуiterator , называя hasNext () и вызов следующей () , чтобы получить значение; Проблема с производительностью возникает только в том случае, если вы используете что-то, что реализует RandomomAccess.

for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
   CustomObj custObj = iter.next();
   ....
}

Проблемы с производительностью цикла, основанного на итераторах, заключаются в следующем:

  1. выделение объекта, даже если список пуст ( Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() во время каждой итерации цикла происходит виртуальный вызов invokeInterface (просмотрите все классы, затем выполните поиск таблицы методов перед переходом).
  3. реализация итератора должна выполнить поиск по крайней мере в 2 полях, чтобы значение для hasNext()вызова было равно значению: # 1 получить текущий счет и # 2 получить общий счет
  4. внутри цикла тела есть еще один виртуальный вызов invokeInterface iter.next(так что: просмотрите все классы и выполните поиск таблицы методов перед переходом), а также должен выполнить поиск по полям: # 1 получить индекс, а # 2 получить ссылку на массив, чтобы сделать смещение в него (в каждой итерации).

Потенциальная оптимизация состоит в том, чтобы переключиться наindex iteration поиск с кэшированным размером:

for(int x = 0, size = customList.size(); x < size; x++){
  CustomObj custObj = customList.get(x);
  ...
}

Здесь мы имеем:

  1. один вызов виртуального метода invokeInterface customList.size()при первоначальном создании цикла for для получения размера
  2. вызов метода get customList.get(x)во время тела для цикла, который является полем поиска в массиве, а затем может сделать смещение в массиве

Мы сократили тонну вызовов методов, полевых поисков. Это вы не хотите делать с LinkedListчем-то или с чем-то, что не является RandomAccessобъектом коллекции, иначе customList.get(x)оно превратится во что-то, что должно проходить через LinkedListкаждую итерацию.

Это идеально, если вы знаете, что это любая RandomAccessколлекция списков.

denis_lor
источник
1

foreachвсе равно использует итераторы под капотом. Это действительно просто синтаксический сахар.

Рассмотрим следующую программу:

import java.util.List;
import java.util.ArrayList;

public class Whatever {
    private final List<Integer> list = new ArrayList<>();
    public void main() {
        for(Integer i : list) {
        }
    }
}

Давайте скомпилировать его с javac Whatever.java,
и читать разобранные байты - код main(), с помощью javap -c Whatever:

public void main();
  Code:
     0: aload_0
     1: getfield      #4                  // Field list:Ljava/util/List;
     4: invokeinterface #5,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
     9: astore_1
    10: aload_1
    11: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
    16: ifeq          32
    19: aload_1
    20: invokeinterface #7,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
    25: checkcast     #8                  // class java/lang/Integer
    28: astore_2
    29: goto          10
    32: return

Мы можем видеть, что это foreachсводится к программе, которая:

  • Создает итератор, используя List.iterator()
  • If Iterator.hasNext(): вызывает Iterator.next()и продолжает цикл

Что касается того, «почему этот бесполезный цикл не оптимизируется из скомпилированного кода? Мы видим, что он ничего не делает с элементом списка»: хорошо, вы можете написать свой повторяемый код, .iterator()имеющий побочные эффекты. или так, что .hasNext()имеет побочные эффекты или значимые последствия.

Вы можете легко представить, что итератор, представляющий прокручиваемый запрос из базы данных, может сделать что-то существенное .hasNext()(например, связаться с базой данных или закрыть курсор, потому что вы достигли конца набора результатов).

Таким образом, хотя мы можем доказать, что в теле цикла ничего не происходит ... это дороже (неразрешимо?) Доказать, что ничего существенного / последовательного не происходит, когда мы выполняем итерацию. Компилятор должен оставить это пустое тело цикла в программе.

Лучшее, на что мы могли надеяться, это предупреждение компилятора . Интересно , что javac -Xlint:all Whatever.javaэто не предупреждает нас об этом пустом теле цикла. IntelliJ IDEA делает, хотя. По общему признанию я настроил IntelliJ для использования Eclipse Compiler, но это не может быть причиной этого.

введите описание изображения здесь

Birchlabs
источник
0

Iterator - это интерфейс в платформе Java Collections, который предоставляет методы для обхода или перебора коллекции.

Итератор и цикл for действуют аналогично, когда ваш мотив состоит в том, чтобы просто пройти по коллекции, чтобы прочитать ее элементы.

for-each это всего лишь один из способов перебора коллекции.

Например:

List<String> messages= new ArrayList<>();

//using for-each loop
for(String msg: messages){
    System.out.println(msg);
}

//using iterator 
Iterator<String> it = messages.iterator();
while(it.hasNext()){
    String msg = it.next();
    System.out.println(msg);
}

И цикл for-each может использоваться только для объектов, реализующих интерфейс итератора.

Теперь вернемся к случаю цикла for и итератора.

Разница возникает, когда вы пытаетесь изменить коллекцию. В этом случае итератор более эффективен из-за своего свойства fail-fast . то есть. он проверяет наличие каких-либо изменений в структуре базовой коллекции перед тем, как перейти к следующему элементу. Если будут найдены какие-либо изменения, будет выдано исключение ConcurrentModificationException .

(Примечание: эта функциональность итератора применима только в случае классов коллекций в пакете java.util. Она не применима для параллельных коллекций, поскольку они по своей природе отказоустойчивы)

eccentricCoder
источник
1
Ваше утверждение о разнице не верно, для каждого цикла также используется итератор под водой, и поэтому он ведет себя одинаково.
Пол Уогланд
@Pault Wagland, я изменил свой ответ, спасибо, что указали на ошибку
eccentricCoder
Ваши обновления все еще не точны. Два фрагмента кода, которые у вас есть, определены языком как одинаковые. Если есть разница в поведении, это ошибка в реализации. Разница лишь в том, есть ли у вас доступ к итератору.
Пол Уогланд
@Paul Wagland Даже если вы используете реализацию по умолчанию для каждого цикла, который использует итератор, он все равно выдаст и исключение, если вы попытаетесь использовать метод remove () во время параллельных операций. Проверьте следующее для получения дополнительной информации здесь
eccentricCoder
1
с каждым циклом вы не получаете доступ к итератору, поэтому вы не можете вызвать на нем команду remove. Но это не относится к делу, в своем ответе вы утверждаете, что один потокобезопасен, а другой - нет. В соответствии со спецификацией языка они эквивалентны, поэтому они оба настолько же потоко-безопасны, как и базовые коллекции.
Пол Уогланд
-8

Мы должны избегать использования традиционного цикла for при работе с коллекциями. Простая причина, которую я приведу, состоит в том, что сложность цикла for имеет порядок O (sqr (n)), а сложность Iterator или даже расширенного цикла for равна O (n). Таким образом, это дает разницу в производительности. Просто возьмите список из примерно 1000 наименований и распечатайте его обоими способами. а также распечатать разницу во времени для исполнения. Вы можете увидеть разницу.

Chandan
источник
пожалуйста, добавьте несколько иллюстративных примеров в поддержку ваших заявлений.
Раджеш Питти
@Chandan Извините, но то, что вы написали, неверно. Например: std :: vector также является коллекцией, но его доступ стоит O (1). Таким образом, традиционный цикл for над вектором это просто O (n). Я думаю, вы хотите сказать, что если доступ к нижележащему контейнеру имеет стоимость доступа O (n), то есть для std :: list, чем сложность O (n ^ 2). Использование итераторов в этом случае снизит стоимость до O (n), поскольку итераторы обеспечивают прямой доступ к элементам.
Кайзер
Если вы выполняете расчет разницы во времени, убедитесь, что оба набора отсортированы (или распределены случайным образом, не отсортированы), и выполните тест дважды для каждого набора и рассчитайте второй прогон только для каждого. Проверьте это еще раз (это длинное объяснение того, почему вам нужно выполнить тест дважды). Вы должны продемонстрировать (возможно, с кодом), как это правда. В остальном, насколько я знаю, оба идентичны с точки зрения производительности, но не возможностей.
Ёдобонеби