Qual è più efficiente, un ciclo for-each o un iteratore?


206

Qual è il modo più efficace per attraversare una collezione?

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

o

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

Si noti che questo non è un duplicato esatto di questo , questo , questo o questo , sebbene una delle risposte all'ultima domanda si avvicini. Il motivo per cui questo non è un duplicato è che la maggior parte di questi sta confrontando i loop in cui si chiama get(i)all'interno del loop, piuttosto che usare l'iteratore.

Come suggerito su Meta, pubblicherò la mia risposta a questa domanda.


Penserei che non faccia differenza dal momento che Java e il meccanismo di modellazione sono poco più che zucchero sintattico
Hassan Syed,


2
@OMG Pony: non credo che si tratti di un duplicato, dal momento che non confronta il ciclo con l'iteratore, ma chiede piuttosto perché le raccolte restituiscono iteratori, piuttosto che avere gli iteratori direttamente sulla classe stessa.
Paul Wagland,

Risposte:


264

Se stai semplicemente vagando sulla raccolta per leggere tutti i valori, allora non c'è alcuna differenza tra l'utilizzo di un iteratore o la nuova sintassi del ciclo, poiché la nuova sintassi utilizza solo l'iteratore sott'acqua.

Se tuttavia, intendi per loop il vecchio loop "c-style":

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

Quindi il nuovo for loop, o iteratore, può essere molto più efficiente, a seconda della struttura dei dati sottostante. La ragione di ciò è che per alcune strutture di dati get(i)è un'operazione O (n), che rende il ciclo un'operazione O (n 2 ). Un elenco tradizionale collegato è un esempio di tale struttura di dati. Tutti gli iteratori hanno come requisito fondamentale che next()dovrebbe essere un'operazione O (1), rendendo il ciclo O (n).

Per verificare che l'iteratore sia utilizzato sott'acqua dalla nuova sintassi del ciclo, confrontare i codici byte generati dai seguenti due frammenti Java. Prima il ciclo 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

E in secondo luogo, l'iteratore:

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

Come puoi vedere, il codice byte generato è effettivamente identico, quindi non vi è alcuna penalità prestazionale nell'uso di entrambi i moduli. Pertanto, dovresti scegliere la forma di loop che è più esteticamente attraente per te, per la maggior parte delle persone che sarà il ciclo for-each, in quanto ha meno codice boilerplate.


4
Credo che stesse dicendo il contrario, che foo.get (i) può essere molto meno efficiente. Pensa a LinkedList. Se si esegue un foo.get (i) nel mezzo di una LinkedList, deve attraversare tutti i nodi precedenti per arrivare a i. Un iteratore, d'altra parte, manterrà una gestione della struttura di dati sottostante e ti permetterà di camminare sui nodi uno alla volta.
Michael Krauklis,

1
Non è una cosa importante, ma anche un for(int i; i < list.size(); i++) {ciclo di stile deve valutare list.size()alla fine di ogni iterazione, se viene utilizzato a volte è più efficiente memorizzare nella cache il risultato del list.size()primo.
Brett Ryan,

3
In realtà, l'istruzione originale è vera anche per il caso di ArrayList e di tutti gli altri che implementano l'interfaccia RandomAccess. Il ciclo "C-style" è più veloce di quello basato su Iterator. docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp

4
Un motivo per usare il vecchio ciclo in stile C piuttosto che l'approccio Iterator, indipendentemente dal fatto che sia la versione foreach o desugar'd, è la spazzatura. Molte strutture di dati creano un'istanza di un nuovo Iterator quando viene chiamato .iterator (), tuttavia è possibile accedervi senza allocazione utilizzando il ciclo in stile C. Questo può essere importante in alcuni ambienti ad alte prestazioni in cui si sta cercando di evitare (a) colpire l'allocatore o (b) raccolte di rifiuti.
Dan,

3
Proprio come un altro commento, per ArrayLists, il ciclo for (int i = 0 ....) è circa 2 volte più veloce rispetto all'utilizzo dell'iteratore o dell'approccio for (:), quindi dipende davvero dalla struttura sottostante. E come nota a margine, l'iterazione di HashSet è anche molto costosa (molto più di una lista di array), quindi evita quelli come la peste (se puoi).
Leone,

106

La differenza non è nelle prestazioni, ma nelle capacità. Quando si utilizza direttamente un riferimento, si ha più potere sull'uso esplicito di un tipo di iteratore (ad esempio List.iterator () vs. List.listIterator (), sebbene nella maggior parte dei casi restituiscano la stessa implementazione). Hai anche la possibilità di fare riferimento all'Iteratore nel tuo ciclo. Ciò ti consente di fare cose come rimuovere elementi dalla tua raccolta senza ottenere ConcurrentModificationException.

per esempio

Questo va bene:

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

Questo non lo è, poiché genererà un'eccezione di modifica simultanea:

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
Questo è molto vero, anche se non risponde direttamente alla domanda che gli ho dato +1 per essere informativo e rispondere alla domanda di follow-on logica.
Paul Wagland,

1
Sì, possiamo accedere agli elementi della raccolta con ciclo foreach, ma non possiamo rimuoverli, ma possiamo rimuovere gli elementi con Iterator.
Akash5288,

22

Per espandere la risposta di Paul, ha dimostrato che il bytecode è lo stesso su quel particolare compilatore (presumibilmente il javac di Sun?) Ma non è garantito che compilatori diversi generino lo stesso bytecode, giusto? Per vedere qual è la differenza effettiva tra i due, andiamo direttamente alla fonte e controlliamo le specifiche del linguaggio Java, in particolare 14.14.2, "L'istruzione migliorata per" :

L' foristruzione migliorata equivale a forun'istruzione di base del modulo:

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

In altre parole, il JLS richiede che i due siano equivalenti. In teoria ciò potrebbe significare differenze marginali nel bytecode, ma in realtà è necessario il loop avanzato per:

  • Richiama il .iterator()metodo
  • Uso .hasNext()
  • Rendi disponibile la variabile locale tramite .next()

Quindi, in altre parole, per tutti gli scopi pratici il bytecode sarà identico o quasi identico. È difficile prevedere l'implementazione di un compilatore che comporterebbe una differenza significativa tra i due.


In realtà, il test che ho fatto è stato con il compilatore Eclipse, ma il tuo punto generale rimane valido. +1
Paul Wagland,

3

Il foreachunderhood sta creandoiterator , chiamando hasNext () e chiamando next () per ottenere il valore; Il problema con le prestazioni si presenta solo se si sta utilizzando qualcosa che implementa RandomomAccess.

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

I problemi di prestazioni con il ciclo basato su iteratore sono dovuti a:

  1. allocare un oggetto anche se l'elenco è vuoto ( Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() durante ogni iterazione del ciclo c'è una chiamata virtuale invokeInterface (passa attraverso tutte le classi, quindi cerca la tabella dei metodi prima del salto).
  3. l'implementazione dell'iteratore deve fare almeno 2 ricerche nei campi per fare in modo che il numero di hasNext()chiamata sia il valore: # 1 ottiene il conteggio corrente e # 2 ottiene il conteggio totale
  4. all'interno del body body, c'è un'altra chiamata virtuale invokeInterface iter.next(quindi: esamina tutte le classi e fai la ricerca della tabella dei metodi prima del salto) e deve anche cercare i campi: # 1 ottiene l'indice e # 2 ottieni il riferimento al array per eseguire l'offset al suo interno (in ogni iterazione).

Una potenziale ottimizzazione è passare a unindex iteration con la ricerca della dimensione memorizzata nella cache:

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

Qui abbiamo:

  1. una chiamata al metodo virtuale invokeInterface customList.size()sulla creazione iniziale del ciclo for per ottenere la dimensione
  2. la chiamata del metodo get customList.get(x)durante il body per il ciclo, che è una ricerca di campo nell'array e quindi può eseguire l'offset nell'array

Abbiamo ridotto un sacco di chiamate di metodo, ricerche sul campo. Questo non si vuole fare con LinkedListo con qualcosa che non è un oggetto di RandomAccessraccolta, altrimenti customList.get(x)si trasformerà in qualcosa che deve attraversare LinkedListogni iterazione.

Questo è perfetto quando sai che c'è una RandomAccessraccolta di elenchi basata.


1

foreachutilizza comunque iteratori sotto il cofano. È davvero solo zucchero sintattico.

Considera il seguente programma:

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

Compiliamolo con javac Whatever.java,
e leggiamo il bytecode disassemblato di main(), usando 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

Possiamo vedere che si foreachcompila in un programma che:

  • Crea iteratore usando List.iterator()
  • If Iterator.hasNext(): richiama Iterator.next()e continua il ciclo

Per quanto riguarda "perché questo loop inutile non viene ottimizzato dal codice compilato? Possiamo vedere che non fa nulla con l'elemento dell'elenco": beh, è ​​possibile per te codificare il tuo iterabile in modo tale che .iterator()abbia effetti collaterali o così che .hasNext()ha effetti collaterali o conseguenze significative.

Potresti facilmente immaginare che un iterabile che rappresenta una query scorrevole da un database potrebbe fare qualcosa di drammatico .hasNext()(come contattare il database o chiudere un cursore perché hai raggiunto la fine del set di risultati).

Quindi, anche se possiamo dimostrare che non succede nulla nel corpo del loop ... è più costoso (intrattabile?) Dimostrare che non accade nulla di significativo / consequenziale quando ripetiamo. Il compilatore deve lasciare questo corpo di loop vuoto nel programma.

Il meglio che possiamo sperare sarebbe un avvertimento del compilatore . È interessante che javac -Xlint:all Whatever.javanon ci avverta di questo corpo ad anello vuoto. IntelliJ IDEA lo fa. Devo ammettere che ho configurato IntelliJ per usare Eclipse Compiler, ma potrebbe non essere questo il motivo.

inserisci qui la descrizione dell'immagine


0

Iterator è un'interfaccia nel framework Collezioni Java che fornisce metodi per attraversare o iterare una raccolta.

Sia iterator che for loop si comportano in modo simile quando il tuo motivo è quello di attraversare una raccolta per leggere i suoi elementi.

for-each è solo un modo per scorrere la raccolta.

Per esempio:

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

E per ogni ciclo può essere utilizzato solo su oggetti che implementano l'interfaccia iteratore.

Ora torniamo al caso di for loop e iteratore.

La differenza viene quando si tenta di modificare una raccolta. In questo caso, l'iteratore è più efficiente a causa della sua proprietà fail-fast . vale a dire. verifica eventuali modifiche nella struttura della raccolta sottostante prima di passare all'elemento successivo. Se vengono rilevate modifiche, verrà generata ConcurrentModificationException .

(Nota: questa funzionalità di iteratore è applicabile solo in caso di classi di raccolta nel pacchetto java.util. Non è applicabile per raccolte simultanee in quanto sono a prova di errore per natura)


1
La tua affermazione sulla differenza non è vera, per ogni ciclo usa anche un iteratore sott'acqua, e quindi ha lo stesso comportamento.
Paul Wagland,

@Pault Wagland, ho modificato la mia risposta grazie per aver segnalato l'errore
eccentricCoder

i tuoi aggiornamenti non sono ancora accurati. I due frammenti di codice che hai sono definiti dalla lingua per essere gli stessi. Se c'è qualche differenza nel comportamento che è un bug nell'implementazione. L'unica differenza è se hai o meno accesso all'iteratore.
Paul Wagland,

@Paul Wagland Anche se si utilizza l'implementazione predefinita di per ogni ciclo che utilizza un iteratore, genererà comunque un'eccezione se si tenta di utilizzare il metodo remove () durante operazioni simultanee. Controlla qui di seguito per ulteriori informazioni qui
eccentricCoder

1
con il per ogni ciclo, non si ottiene l'accesso all'iteratore, quindi non è possibile chiamare rimuovere su di esso. Ma questo è il punto, nella tua risposta affermi che uno è thread-safe, mentre l'altro no. Secondo le specifiche del linguaggio sono equivalenti, quindi sono entrambi sicuri per il thread quanto le raccolte sottostanti.
Paul Wagland,

-8

Dovremmo evitare di usare il tradizionale ciclo per lavorare con le raccolte. La semplice ragione che darò è che la complessità di for loop è dell'ordine O (sqr (n)) e la complessità di Iterator o anche il potenziato per loop è solo O (n). Quindi dà una differenza di prestazioni .. Basta prendere un elenco di circa 1000 articoli e stamparlo in entrambi i modi. e anche stampare la differenza di tempo per l'esecuzione. Puoi vedere la differenza.


si prega di aggiungere alcuni esempi illustrativi a supporto delle dichiarazioni.
Rajesh Pitty,

@Chandan Siamo spiacenti ma ciò che hai scritto è sbagliato. Ad esempio: std :: vector è anche una raccolta ma il suo accesso costa O (1). Quindi un ciclo tradizionale per un vettore è solo O (n). Penso che tu voglia dire, se l'accesso al contenitore sottostante ha un costo di accesso di O (n), quindi è per std :: list, che c'è una complessità di O (n ^ 2). L'uso degli iteratori in quel caso ridurrà il costo in O (n), poiché gli iteratori consentono l'accesso diretto agli elementi.
Kaiser

Se si esegue il calcolo della differenza di tempo, assicurarsi che entrambi i set siano ordinati (o distribuiti casualmente non distribuiti in modo casuale) ed eseguire il test due volte per ciascun set e calcolare solo la seconda serie di ciascuno. Controlla di nuovo i tuoi tempi con questo (è una lunga spiegazione del perché è necessario eseguire il test due volte). Devi dimostrare (forse con il codice) come questo sia vero. Altrimenti per quanto ne so entrambi sono identici in termini di prestazioni, ma non di capacità.
ydobonebi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.