Що є більш ефективним, для кожного циклу чи ітератором?


206

Який найефективніший спосіб перемістити колекцію?

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: Я не вірю, що це дублікат, оскільки це не порівнює цикл з ітератором, а швидше запитує, чому колекції повертають ітератори, а не мати ітератори безпосередньо на самому класі.
Пол Вагленд

Відповіді:


264

Якщо ви просто блукаєте над колекцією, щоб прочитати всі значення, то різниці між використанням ітератора чи новим для синтаксису циклу немає, оскільки новий синтаксис просто використовує ітератор під водою.

Якщо ви маєте на увазі під циклом старий цикл "c-style":

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

Тоді новий цикл або ітератор можуть бути набагато ефективнішими, залежно від основної структури даних. Причиною цього є те, що для деяких структур даних 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

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


4
Я вважаю, що він говорив навпаки, що foo.get (i) може бути набагато менш ефективним. Подумайте про LinkedList. Якщо ви робите foo.get (i) посередині LinkedList, він повинен пройти всі попередні вузли, щоб дістатися до i. Ітератор, з іншого боку, буде тримати ручку основної структури даних і дозволить вам переходити по вузлах один за одним.
Майкл Краукліс

1
Не велика річ, але for(int i; i < list.size(); i++) {цикл стилів також повинен оцінюватися list.size()в кінці кожної ітерації, якщо вона використовується, іноді ефективніше кешувати результат list.size()першого.
Бретт Райан

3
Власне, оригінальне твердження також стосується випадку ArrayList та всіх інших, які реалізують інтерфейс RandomAccess. Цикл "С-стилю" швидший, ніж на основі Iterator. docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp

4
Однією з причин використовувати старий цикл у стилі С, а не підхід Iterator, незалежно від того, чи це версія foreach чи desugar'd, - це сміття. Багато структур даних створюють новий ітератор, коли викликається .iterator (), однак до них можна отримати доступ без виділення, використовуючи цикл стилю C. Це може бути важливим у деяких високоефективних умовах, коли намагаються уникати (a) попадання на розподільник або (b) збирання сміття.
День

3
Як і ще один коментар, для ArrayLists цикл for (int i = 0 ....) приблизно в 2 рази швидший, ніж використання ітератора або підходу for (:), тому він дійсно залежить від основної структури. І як зауваження, ітерація HashSets також дуже дорога (набагато більше, ніж список масивів), тому уникайте таких, як чума (якщо можете).
Лев

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.
Акаш5288

22

Щоб розширити власну відповідь Поля, він продемонстрував, що байт-код однаковий для цього конкретного компілятора (імовірно, 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()

Отже, іншими словами, для всіх практичних цілей байт-код буде ідентичним, або майже ідентичним. Важко передбачити будь-яку реалізацію компілятора, яка б призвела до суттєвої різниці між ними.


Насправді тест, який я робив, був у компіляторі 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. Для виконання ітератора необхідно виконати пошук щонайменше у двох полях, щоб 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. один віртуальний метод виклику інтерфейсу викликає customList.size()початкове створення циклу for для отримання розміру
  2. виклик методу get customList.get(x)під час тіла для циклу, який є полем пошуку для масиву, а потім може зробити зсув у масив

Ми зменшили тонну викликів методів, пошук у полі. Цього ви не хочете робити з LinkedListчи з чимось, що не є RandomAccessоб'єктом колекції, інакше customList.get(x)це перетвориться на щось, що має пройти через LinkedListкожну ітерацію.

Це ідеально, коли ви знаєте, що це будь- RandomAccessяка колекція списків.


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()
  • Якщо Iterator.hasNext(): викликає Iterator.next()і продовжує цикл

Що стосується "чому цей марний цикл не оптимізується зі складеного коду? Ми можемо побачити, що він нічого не робить із елементом списку": ну, ви можете кодувати свій ітерабельний такий, що .iterator()має побічні ефекти або так, що .hasNext()має побічні ефекти або значущі наслідки.

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

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

Найкраще , що ми могли сподіватися б компілятор попередження . Цікаво , що javac -Xlint:all Whatever.javaце НЕ попереджає нас про це порожньому тілі циклу. IntelliJ IDEA робить це. Справді, я налаштував IntelliJ на використання компілятора Eclipse, але це може бути не причиною.

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


0

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

І для-кожного циклу можна користуватися лише на об'єктах, що реалізують інтерфейс ітератора.

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

Різниця виникає при спробі змінити колекцію. У цьому випадку ітератор є більш ефективним через свою невдалу властивість . тобто. він перевіряє будь-які зміни в структурі базової колекції перед ітерацією над наступним елементом. Якщо знайдеться якісь модифікації, це викине ConcurrentModificationException .

(Примітка. Ця функціональність ітератора застосована лише у випадках, коли класи колекцій в пакеті java.util. Він не застосовується для одночасних колекцій, оскільки вони за своєю природою не захищені)


1
Ваше твердження про різницю не відповідає дійсності, тому що для кожного циклу також використовується ітератор під водою, і тому така ж поведінка.
Пол Вагленд

@Pault Wagland, я змінив свою відповідь спасибі за вказівку на помилку
eccentricCoder

ваші оновлення все ще не точні. Два фрагменти коду, які ви вказали, визначаються мовою, що є однаковою. Якщо є якась різниця в поведінці, це помилка в реалізації. Різниця полягає лише в тому, чи маєте ви доступ до ітератора.
Пол Вагленд

@Paul Wagland Навіть якщо ви використовуєте реалізацію за замовчуванням для кожного циклу, який використовує ітератор, він все одно буде кинутим і винятковим, якщо ви спробуєте використовувати метод delete () під час одночасних операцій. Ознайомтесь із наступним, щоб отримати докладнішу інформацію тут
eccentricCoder

1
з циклом для кожного циклу ви не отримуєте доступу до ітератора, тому не можете викликати його видалення. Але це поруч із точкою, у своїй відповіді ви стверджуєте, що один є безпечним для потоків, а інший - ні. Відповідно до мовної специфікації, вони рівноцінні, тому обидва вони є настільки ж безпечними для потоків, як і базові колекції.
Пол Вагленд

-8

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


будь-ласка, додайте кілька наочних прикладів для підтвердження ваших тверджень.
Rajesh Pitty

@Chandan Вибачте, але те, що ви написали, неправильно. Наприклад: std :: vector - це також колекція, але її доступ коштує O (1). Отже, традиційним для циклу над вектором є просто O (n). Я думаю, ви хочете сказати, якщо доступ до нижнього контейнера має вартість доступу O (n), значить, для std :: list є складність O (n ^ 2). Використання ітераторів у цьому випадку зменшить вартість до O (n), оскільки ітератори дозволяють отримати прямий доступ до елементів.
кайзер

Якщо ви робите підрахунок різниці у часі, переконайтесь, що обидва набори відсортовані (або справедливо розподілені випадковими несортованими) та проведіть тест двічі для кожного набору та обчисліть другий запуск кожного лише. Знову перевірте свої таймінги (це довге пояснення того, чому потрібно запустити тест двічі). Вам потрібно продемонструвати (можливо, з кодом), як це правда. Інакше, наскільки я знаю, обидва однакові за продуктивністю, але не за можливостями.
ydobonebi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.