web-dev-qa-db-fra.com

Qu'est-ce qui est plus efficace, une boucle pour chaque boucle ou un itérateur?

Quel est le moyen le plus efficace de parcourir une collection?

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

ou

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

S'il vous plaît noter que ce n'est pas une copie exacte de this , this , this , ou this , bien qu'un des réponses à la dernière question se rapproche. La raison pour laquelle ce n'est pas une dupe, c'est que la plupart d'entre elles comparent des boucles dans lesquelles vous appelez get(i) à l'intérieur de la boucle, plutôt que d'utiliser l'itérateur.

Comme suggéré sur Meta , je posterai ma réponse à cette question.

193
Paul Wagland

Si vous parcourez simplement la collection pour lire toutes les valeurs, il n'y a aucune différence entre utiliser un itérateur et la nouvelle syntaxe de boucle for, car la nouvelle syntaxe utilise simplement l'itérateur sous-marin.

Si toutefois, vous entendez par boucle l'ancienne boucle "c-style":

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

Ensuite, la nouvelle boucle for, ou itérateur, peut être beaucoup plus efficace, en fonction de la structure de données sous-jacente. La raison en est que pour certaines structures de données, get(i) est une opération O(n), ce qui fait de la boucle un O (n2) opération. Une liste chaînée traditionnelle est un exemple d'une telle structure de données. Tous les itérateurs ont pour exigence fondamentale que next() soit une opération O(1), créant ainsi la boucle O (n).

Pour vérifier que l'itérateur est utilisé sous l'eau par la nouvelle syntaxe de boucle for, comparez les bytecodes générés à partir des deux extraits Java suivants. D'abord la boucle 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

Et deuxièmement, l'itérateur:

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

Comme vous pouvez le constater, le code d'octet généré est en réalité identique, il n'y a donc aucune pénalité de performance à utiliser l'un ou l'autre de ces formulaires. Par conséquent, vous devriez choisir la forme de boucle la plus attrayante du point de vue esthétique, pour la plupart des gens, ce sera la boucle pour-chaque, car elle a moins de code passe-partout.

254
Paul Wagland

La différence ne réside pas dans les performances, mais dans les capacités. Lorsque vous utilisez directement une référence, vous avez plus de pouvoir sur l'utilisation explicite d'un type d'itérateur (par exemple, List.iterator () contre List.listIterator (), bien que dans la plupart des cas, ils retournent la même implémentation). Vous avez également la possibilité de référencer l'Iterator dans votre boucle. Cela vous permet de supprimer des éléments de votre collection sans obtenir une exception ConcurrentModificationException.

par exemple.

C'est acceptable:

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

Ce n'est pas le cas, car une exception de modification simultanée sera levée:

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

for(Object o : set){
     if(o meets some condition){
          set.remove(o);
     }
}
102
Michael Krauklis

Pour développer la réponse de Paul, il a démontré que le bytecode est identique sur ce compilateur particulier (vraisemblablement le javac de Sun?), Mais que différents compilateurs ne sont pas garantis générer le même bytecode, non? Pour voir quelle est la différence entre les deux, passons directement à la source et examinons la spécification de langage Java, plus précisément 14.14.2, "L'instruction améliorée pour" :

L'instruction améliorée for équivaut à une instruction de base for de la forme:

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

En d'autres termes, le JLS exige que les deux soient équivalents. En théorie, cela pourrait signifier des différences marginales dans le bytecode, mais en réalité, la boucle for améliorée est nécessaire pour:

  • Invoquer la méthode .iterator()
  • Utilisez .hasNext()
  • Rendre la variable locale disponible via .next()

Donc, en d'autres termes, à toutes fins pratiques, le bytecode sera identique ou presque identique. Il est difficile d’envisager une implémentation du compilateur qui entraînerait une différence significative entre les deux.

22
Cowan

Le foreach underhood crée la iterator, appelle hasNext () et appelle next () pour obtenir la valeur; Le problème avec la performance ne vient que si vous utilisez quelque chose qui implémente RandomomAccess.

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

Les problèmes de performances avec la boucle basée sur les itérateurs sont les suivants:

  1. allouer un objet même si la liste est vide (Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() lors de chaque itération de la boucle, il existe un appel virtuel invokeInterface (parcourez toutes les classes, puis effectuez une recherche dans la table des méthodes avant le saut).
  3. l'implémentation de l'itérateur doit faire au moins 2 recherches de champs pour que hasNext() call calcule la valeur: # 1 obtenir le nombre actuel et # 2 obtenir le nombre total
  4. à l'intérieur de la boucle de corps, il y a un autre appel virtuel invokeInterface iter.next (donc: passez en revue toutes les classes et effectuez une recherche de table de méthodes avant le saut) et effectuez également une recherche de champs: # 1 obtenir l'index et # 2 obtenir la référence au tableau pour y faire le décalage (à chaque itération).

ne optimisation potentielle consiste à passer à un index iteration avec la recherche de taille en cache:

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

Ici nous avons:

  1. un appel de méthode virtuelle invokeInterface customList.size() lors de la création initiale de la boucle for pour obtenir la taille
  2. l'appel de la méthode get customList.get(x) pendant le corps de la boucle, qui est une recherche de champ dans le tableau et peut ensuite effectuer le décalage dans le tableau

Nous avons réduit une tonne d'appels de méthodes et de recherches sur le terrain. Ce que vous ne voulez pas faire avec LinkedList ou avec quelque chose qui n'est pas une collection RandomAccess, sinon la customList.get(x) va se transformer en quelque chose qui doit traverser la LinkedList à chaque itération.

C'est parfait lorsque vous savez qu'il s'agit d'une collection de listes basée sur RandomAccess.

2
denis_lor

foreach utilise quand même des itérateurs. C'est vraiment du sucre syntaxique.

Considérez le programme suivant:

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

Compilons-le avec javac Whatever.Java,
Et lisez le bytecode désassemblé de main(), en utilisant 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

Nous pouvons voir que foreach est compilé en un programme qui:

  • Crée un itérateur à l'aide de List.iterator()
  • Si Iterator.hasNext(): invoque Iterator.next() et continue la boucle

En ce qui concerne "pourquoi cette boucle inutile n'est-elle pas optimisée en dehors du code compilé? Nous pouvons voir qu'elle ne fait rien avec l'élément de liste": eh bien, il vous est possible de coder votre itérable de telle sorte que .iterator() a des effets secondaires, ou de sorte que .hasNext() ait des effets secondaires ou des conséquences importantes.

Vous pouvez facilement imaginer qu'une requête itérative représentant une requête à défilement à partir d'une base de données pourrait avoir un effet dramatique sur .hasNext() (comme contacter la base de données ou fermer un curseur car la fin du jeu de résultats est atteinte).

Ainsi, même si nous pouvons prouver que rien ne se passe dans le corps de la boucle… il est plus coûteux (insoluble?) De prouver que rien de significatif/de conséquence ne se produit lorsque nous itérons. Le compilateur doit laisser ce corps de boucle vide dans le programme.

Le mieux que nous puissions espérer serait un avertissement du compilateur . Il est intéressant de noter que javac -Xlint:all Whatever.Java ne nous avertit pas à propos de ce corps de boucle vide. IntelliJ IDEA le fait cependant. Certes, j'ai configuré IntelliJ pour utiliser Eclipse Compiler, mais ce n'est peut-être pas la raison.

enter image description here

1
Birchlabs

Iterator est une interface du cadre Java Collections qui fournit des méthodes pour parcourir ou itérer une collection.

L'itérateur et la boucle for fonctionnent de manière similaire lorsque votre objectif est de parcourir une collection pour en lire les éléments.

for-each n'est qu'un moyen de parcourir la collection.

Par exemple:

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

Et pour chaque boucle ne peut être utilisé que sur des objets implémentant l'interface itérateur.

Revenons maintenant au cas de la boucle for et de l'itérateur.

La différence vient lorsque vous essayez de modifier une collection. Dans ce cas, l'itérateur est plus efficace en raison de sa propriété fail-fast . c'est à dire. il vérifie toute modification dans la structure de la collection sous-jacente avant d'itérer le prochain élément. Si des modifications ont été trouvées, le message ConcurrentModificationException.

(Remarque: cette fonctionnalité de l'itérateur n'est applicable que dans le cas de classes de collection dans le package Java.util. Elle ne s'applique pas aux collections simultanées car elles sont intrinsèquement à sécurité intrinsèque.)

0
eccentricCoder