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

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

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();
}

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

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

Ответы

Ответ 1

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

Если, однако, вы зацикливаете цикл "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).

Чтобы убедиться, что итератор используется под водой с помощью нового синтаксиса цикла, сравните сгенерированные байт-коды из следующих двух фрагментов 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

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

Ответ 2

Разница заключается не в производительности, а в возможностях. При непосредственном использовании ссылки у вас больше полномочий явно используется тип итератора (например, List.iterator() и List.listIterator(), хотя в большинстве случаев они возвращают ту же реализацию). У вас также есть возможность ссылаться на Iterator в вашем цикле. Это позволяет делать такие вещи, как удаление элементов из вашей коллекции без получения исключения 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);
     }
}

Ответ 3

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

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

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

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

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

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

Ответ 4

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

for (String i : list) {
    System.out.println(i);
    list.remove(i); // throws exception
} 

Iterator it=list.iterator();
while (it.hasNext()){
    System.out.println(it.next());
    it.remove(); // valid here
}

Ответ 5

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

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

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);
}

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

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

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

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

Ответ 6

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()
  • Если Iterator.hasNext(): вызывает Iterator.next() и продолжает цикл

Что касается "почему этот бесполезный цикл не оптимизирован из скомпилированного кода, мы можем видеть, что он ничего не делает с элементом списка": ну, вы можете запрограммировать свой итерабельный, чтобы .iterator() имеет побочные эффекты или что .hasNext() имеет побочные эффекты или значимые последствия.

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

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

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

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

Ответ 7

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