Java 8 Iterable.forEach () vs ciclo foreach

Quale delle seguenti è una pratica migliore in Java 8?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join)); 

Java 7:

 for (String join : joins) { mIrc.join(mSession, join); } 

Ho molti cicli che potrebbero essere “semplificati” con lambda, ma c’è davvero qualche vantaggio nell’utilizzarli, comprese le prestazioni e la leggibilità?

MODIFICARE

Estenderò anche questa domanda a metodi più lunghi – so che non puoi restituire o interrompere la funzione genitore da una lambda e questo dovrebbe essere menzionato a se sono confrontati, ma c’è qualcos’altro da considerare?

Il vantaggio viene preso in considerazione quando le operazioni possono essere eseguite in parallelo. (Vedi http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and – la sezione sull’iterazione interna ed esterna)

  • Il principale vantaggio dal mio punto di vista è che l’implementazione di ciò che deve essere fatto all’interno del loop può essere definita senza dover decidere se verrà eseguita in parallelo o in sequenza

  • Se vuoi che il tuo ciclo sia eseguito in parallelo, potresti semplicemente scrivere

      joins.parallelStream().forEach(join -> mIrc.join(mSession, join)); 

    Dovrai scrivere un codice aggiuntivo per la gestione dei thread, ecc.

Nota: per la mia risposta ho ipotizzato join che implementano l’interfaccia java.util.Stream . Se join implementa solo l’interfaccia java.util.Iterable questo non è più vero.

La pratica migliore è usare for-each . Oltre a violare il principio di Keep It Simple, Stupid , il new- forEach() ha almeno le seguenti carenze:

  • Non posso usare variabili non definitive . Quindi, codice come il seguente non può essere trasformato in un lambda forEach:

     Object prev = null; for(Object curr : list) { if( prev != null ) foo(prev, curr); prev = curr; } 
  • Imansible gestire eccezioni controllate . A Lambdas non è in realtà vietato lanciare eccezioni controllate, ma le interfacce funzionali comuni come Consumer non ne dichiarano alcuna. Pertanto, qualsiasi codice che genera eccezioni controllate deve includerle in try-catch o Throwables.propagate() . Ma anche se lo fai, non è sempre chiaro cosa succede all’eccezione generata. Potrebbe essere inghiottito da qualche parte nelle budella di forEach()

  • Controllo del stream limitato . Un return in una lambda equivale a continue in a per-ciascuno, ma non c’è equivalente a una break . È anche difficile fare cose come valori di ritorno, cortocircuiti o flag di impostazione (il che avrebbe alleviato un po ‘le cose, se non si trattava di una violazione della regola delle variabili non definitive ). “Questo non è solo un’ottimizzazione, ma è fondamentale se si considera che alcune sequenze (come la lettura delle linee in un file) possono avere effetti collaterali, o si può avere una sequenza infinita.”

  • Potremmo eseguire in parallelo , che è una cosa orribile, orribile per tutti tranne lo 0,1% del tuo codice che deve essere ottimizzato. È necessario pensare a qualsiasi codice parallelo (anche se non utilizza i blocchi, i volatili e altri aspetti particolarmente sgradevoli dell’esecuzione multi-thread tradizionale). Qualsiasi bug sarà difficile da trovare.

  • Potrebbe danneggiare le prestazioni , perché il JIT non può ottimizzare forEach () + lambda nella stessa misura dei loop normali, specialmente ora che i lambda sono nuovi. Con “ottimizzazione” non intendo il sovraccarico di chiamare lambdas (che è piccolo), ma l’analisi sofisticata e la trasformazione che il compilatore JIT moderno esegue sul codice in esecuzione.

  • Se hai bisogno di parallelismo, è probabilmente molto più veloce e non molto più difficile da usare un ExecutorService . Gli stream sono sia automagical (leggi: non so molto del tuo problema) e usano una strategia di parallelizzazione specializzata (leggi: inefficiente per il caso generale) ( decomposizione ricorsiva di fork-join ).

  • Rende il debug più confuso , a causa della gerarchia di chiamate annidate e, dio non voglia, l’esecuzione parallela. Il debugger potrebbe avere problemi nella visualizzazione di variabili dal codice circostante, e cose come il passaggio-passo potrebbero non funzionare come previsto.

  • Gli stream in generale sono più difficili da codificare, leggere ed eseguire il debug . In realtà, questo è vero per le API ” fluenti ” complesse in generale. La combinazione di singole affermazioni complesse, l’uso massiccio di generici e la mancanza di variabili intermedie contribuiscono a generare confusi messaggi di errore e vanificare il debugging. Invece di “questo metodo non ha un sovraccarico per il tipo X” si ottiene un messaggio di errore più vicino a “da qualche parte in cui hai incasinato i tipi, ma non sappiamo dove o come”. Allo stesso modo, non è ansible esaminare ed esaminare le cose in un debugger con la stessa facilità con cui il codice viene suddiviso in più istruzioni e i valori intermedi vengono salvati nelle variabili. Infine, leggere il codice e comprendere i tipi e il comportamento in ogni fase dell’esecuzione può essere non banale.

  • Si sporge come un pollice dolente . Il linguaggio Java ha già l’istruzione for-each. Perché sostituirlo con una chiamata di funzione? Perché incoraggiare hide gli effetti collaterali da qualche parte nelle espressioni? Perché incoraggiare gli one-liner ingombranti? Miscelazione regolare per-ciascuno e per il nuovo forOgni volente o nolente è di cattivo gusto. Il codice dovrebbe parlare con idiomi (schemi che sono veloci da comprendere a causa della loro ripetizione), e meno idiomi vengono utilizzati, più chiaro è il codice e meno tempo è dedicato a decidere quale idioma usare (una grande perdita di tempo per perfezionisti come me! ).

Come puoi vedere, non sono un grande fan di forEach () tranne nei casi in cui ha senso.

Particolarmente offensivo per me è il fatto che Stream non implementa Iterable (nonostante abbia effettivamente un iterator metodo) e non può essere usato in un for-each, solo con un forEach (). Raccomando di lanciare i flussi in Iterables con stream (Iterable)stream::iterator . Un’alternativa migliore è utilizzare StreamEx che corregge un numero di problemi di Stream API, inclusa l’implementazione di Iterable .

Detto questo, forEach() è utile per quanto segue:

  • Iterazione atomica su una lista sincronizzata . Prima di questo, un elenco generato con Collections.synchronizedList() era atomico rispetto a cose come get o set, ma non era thread-safe durante l’iterazione.

  • Esecuzione parallela (utilizzando un stream parallelo appropriato) . Ciò consente di risparmiare poche righe di codice rispetto all’utilizzo di un ExecutorService, se il tuo problema corrisponde alle ipotesi di prestazioni incorporate in Stream e Splitterator.

  • Contenitori specifici che , come la lista sincronizzata, traggono vantaggio dall’essere in controllo dell’iterazione (sebbene ciò sia in gran parte teorico a meno che le persone non possano richiamare più esempi)

  • Chiamando una singola funzione in modo più pulito usando forEach() e un argomento di riferimento del metodo (ad esempio, list.forEach (obj::someMethod) ). Tuttavia, tieni presente i punti sulle eccezioni controllate, il debugging più complesso e la riduzione del numero di espressioni idiomatiche utilizzate durante la scrittura del codice.

Articoli che ho usato come riferimento:

  • Tutto su Java 8
  • Iterazione dentro e fuori (come indicato da un altro poster)

EDIT: Sembra che alcune delle proposte originali di lambda (come http://www.javac.info/closures-v06a.html ) abbiano risolto alcuni dei problemi citati (aggiungendo naturalmente le proprie complicazioni, ovviamente).

Leggendo questa domanda si può avere l’impressione che Iterable#forEach in combinazione con espressioni lambda sia una scorciatoia / sostituzione per scrivere un ciclo for-each tradizionale. Questo semplicemente non è vero. Questo codice dall’OP:

 joins.forEach(join -> mIrc.join(mSession, join)); 

non è inteso come scorciatoia per la scrittura

 for (String join : joins) { mIrc.join(mSession, join); } 

e certamente non dovrebbe essere usato in questo modo. Invece è inteso come una scorciatoia (anche se non è esattamente la stessa) per la scrittura

 joins.forEach(new Consumer() { @Override public void accept(T join) { mIrc.join(mSession, join); } }); 

Ed è come una sostituzione per il seguente codice Java 7:

 final Consumer c = new Consumer() { @Override public void accept(T join) { mIrc.join(mSession, join); } }; for (T t : joins) { c.accept(t); } 

Sostituire il corpo di un loop con un’interfaccia funzionale, come negli esempi precedenti, rende il tuo codice più esplicito: stai dicendo che (1) il corpo del loop non influenza il codice circostante e il stream di controllo, e (2) il il corpo del loop può essere sostituito con una diversa implementazione della funzione, senza influenzare il codice circostante. Non essere in grado di accedere a variabili non finali dello scope esterno non è un deficit di funzioni / lambda, è una caratteristica che distingue la semantica di Iterable#forEach dalla semantica di un ciclo for-each tradizionale. Una volta che ci si abitua alla syntax di Iterable#forEach , rende il codice più leggibile, poiché si ottiene immediatamente questa informazione aggiuntiva sul codice.

I cicli tradizionali per ogni loop rimarranno sicuramente buone pratiche (per evitare il termine abusato di ” best practice “) in Java. Ma questo non significa che Iterable#forEach dovrebbe essere considerato una ctriggers pratica o uno stile sbagliato. È sempre buona norma usare lo strumento giusto per svolgere il lavoro, e questo include il mixaggio tradizionale per ogni ciclo con Iterable#forEach , dove ha senso.

Poiché gli svantaggi di Iterable#forEach sono già stati discussi in questo thread, ecco alcuni motivi, perché potresti probabilmente voler utilizzare Iterable#forEach :

  • Per rendere il tuo codice più esplicito: come descritto sopra, Iterable#forEach può rendere il tuo codice più esplicito e leggibile in alcune situazioni.

  • Per rendere il codice più estensibile e manutenibile: l’ utilizzo di una funzione come corpo di un ciclo consente di sostituire questa funzione con diverse implementazioni (vedere Schema della strategia ). Ad esempio, potresti sostituire facilmente l’espressione lambda con una chiamata al metodo, che potrebbe essere sovrascritta dalle sottoclassi:

     joins.forEach(getJoinStrategy()); 

    Quindi è ansible fornire strategie predefinite utilizzando un enum, che implementa l’interfaccia funzionale. Ciò non solo rende il tuo codice più estensibile, ma aumenta anche la manutenibilità perché disaccoppia l’implementazione del ciclo dalla dichiarazione del ciclo.

  • Per rendere il tuo codice più debuggabile: separare l’implementazione del ciclo dalla dichiarazione può anche rendere più facile il debug, perché potresti avere un’implementazione di debug specializzata, che stampa i messaggi di debug, senza la necessità di ingombrare il tuo codice principale con if(DEBUG)System.out.println() . L’implementazione del debug potrebbe essere ad esempio un delegato , che decora l’effettiva implementazione della funzione.

  • Per ottimizzare il codice critico delle prestazioni: contrariamente ad alcune delle asserzioni in questo thread, Iterable#forEach fornisce già prestazioni migliori rispetto a un ciclo for-each tradizionale, almeno quando si utilizza ArrayList e si esegue Hotspot in modalità “-client”. Mentre questo aumento delle prestazioni è piccolo e trascurabile per la maggior parte dei casi d’uso, ci sono situazioni in cui questa prestazione extra può fare la differenza. Ad esempio, i manutentori delle librerie vorranno certamente valutare, se alcune delle loro implementazioni di loop esistenti dovrebbero essere sostituite con Iterable#forEach .

    Per sostenere questa affermazione con i fatti, ho fatto alcuni micro-benchmark con Caliper . Ecco il codice di test (è necessario l’ultimo Caliper di git):

     @VmOptions("-server") public class Java8IterationBenchmarks { public static class TestObject { public int result; } public @Param({"100", "10000"}) int elementCount; ArrayList list; TestObject[] array; @BeforeExperiment public void setup(){ list = new ArrayList<>(elementCount); for (int i = 0; i < elementCount; i++) { list.add(new TestObject()); } array = list.toArray(new TestObject[list.size()]); } @Benchmark public void timeTraditionalForEach(int reps){ for (int i = 0; i < reps; i++) { for (TestObject t : list) { t.result++; } } return; } @Benchmark public void timeForEachAnonymousClass(int reps){ for (int i = 0; i < reps; i++) { list.forEach(new Consumer() { @Override public void accept(TestObject t) { t.result++; } }); } return; } @Benchmark public void timeForEachLambda(int reps){ for (int i = 0; i < reps; i++) { list.forEach(t -> t.result++); } return; } @Benchmark public void timeForEachOverArray(int reps){ for (int i = 0; i < reps; i++) { for (TestObject t : array) { t.result++; } } } } 

    E qui ci sono i risultati:

    • Risultati per -client
    • Risultati per -server

    Quando è in esecuzione con "-client", Iterable#forEach sovraperforma il tradizionale ciclo for su ArrayList, ma è ancora più lento rispetto alla iterazione diretta su un array. Quando si esegue con "-server", le prestazioni di tutti gli approcci sono all'incirca uguali.

  • Fornire supporto opzionale per l'esecuzione parallela: è già stato detto qui che la possibilità di eseguire l'interfaccia funzionale di Iterable#forEach in parallelo usando i flussi è certamente un aspetto importante. Poiché Collection#parallelStream() non garantisce che il ciclo venga effettivamente eseguito in parallelo, è necessario considerarlo una funzionalità opzionale . Effettuando list.parallelStream().forEach(...); sull'elenco con list.parallelStream().forEach(...); , dici esplicitamente: questo ciclo supporta l' esecuzione parallela, ma non dipende da esso. Di nuovo, questa è una caratteristica e non un deficit!

    Spostando la decisione per l'esecuzione parallela dall'effettiva implementazione del ciclo, si consente l'ottimizzazione opzionale del codice, senza influenzare il codice stesso, che è una buona cosa. Inoltre, se l'implementazione del stream parallelo predefinito non soddisfa le tue esigenze, nessuno ti impedisce di fornire la tua implementazione. Ad esempio, è ansible fornire una raccolta ottimizzata in base al sistema operativo sottostante, alle dimensioni della raccolta, al numero di core e ad alcune impostazioni delle preferenze:

     public abstract class MyOptimizedCollection implements Collection{ private enum OperatingSystem{ LINUX, WINDOWS, ANDROID } private OperatingSystem operatingSystem = OperatingSystem.WINDOWS; private int numberOfCores = Runtime.getRuntime().availableProcessors(); private Collection delegate; @Override public Stream parallelStream() { if (!System.getProperty("parallelSupport").equals("true")) { return this.delegate.stream(); } switch (operatingSystem) { case WINDOWS: if (numberOfCores > 3 && delegate.size() > 10000) { return this.delegate.parallelStream(); }else{ return this.delegate.stream(); } case LINUX: return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator()); case ANDROID: default: return this.delegate.stream(); } } } 

    La cosa bella è che la tua implementazione del ciclo non ha bisogno di conoscere o preoccuparsi di questi dettagli.

forEach() può essere implementato per essere più veloce di for-each loop, perché l’iterable conosce il modo migliore per iterare i suoi elementi, al contrario del modo iteratore standard. Quindi la differenza è loop internamente o loop esternamente.

Ad esempio ArrayList.forEach(action) può essere semplicemente implementato come

 for(int i=0; i 

al contrario del ciclo for-each che richiede un sacco di scaffolding

 Iterator iter = list.iterator(); while(iter.hasNext()) Object next = iter.next(); do something with `next` 

Tuttavia, dobbiamo anche tenere conto di due costi generali usando forEach() , uno sta creando l'object lambda, l'altro sta invocando il metodo lambda. Probabilmente non sono significativi.

vedere anche http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ per confrontare iterazioni interne / esterne per diversi casi d'uso.

TL; DR : List.stream().forEach() stato il più veloce.

Ho sentito che dovrei aggiungere i miei risultati dall’analisi del benchmarking. Ho adottato un approccio molto semplice (senza schemi di benchmarking) e ho confrontato 5 metodi diversi:

  1. classico for
  2. foreach classico
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

la procedura di test e i parametri

 private List list; private final int size = 1_000_000; public MyClass(){ list = new ArrayList<>(); Random rand = new Random(); for (int i = 0; i < size; ++i) { list.add(rand.nextInt(size * 50)); } } private void doIt(Integer i) { i *= 2; //so it won't get JITed out } 

L'elenco di questa class deve essere ripetuto e avere alcuni doIt(Integer i) applicati a tutti i suoi membri, ogni volta tramite un metodo diverso. nella class Main eseguo il metodo testato tre volte per scaldare la JVM. Quindi eseguo il metodo di test 1000 volte sumndo il tempo necessario per ciascun metodo di iterazione (utilizzando System.nanoTime() ). Fatto questo, divido quella sum per 1000 e questo è il risultato, tempo medio. esempio:

 myClass.fored(); myClass.fored(); myClass.fored(); for (int i = 0; i < reps; ++i) { begin = System.nanoTime(); myClass.fored(); end = System.nanoTime(); nanoSum += end - begin; } System.out.println(nanoSum / reps); 

L'ho eseguito su una CPU i5 4 core, con la versione 1.8.0_05 di java

classico for

 for(int i = 0, l = list.size(); i < l; ++i) { doIt(list.get(i)); } 

tempo di esecuzione: 4,21 ms

foreach classico

 for(Integer i : list) { doIt(i); } 

tempo di esecuzione: 5,95 ms

List.forEach()

 list.forEach((i) -> doIt(i)); 

tempo di esecuzione: 3.11 ms

List.stream().forEach()

 list.stream().forEach((i) -> doIt(i)); 

tempo di esecuzione: 2,79 ms

List.parallelStream().forEach

 list.parallelStream().forEach((i) -> doIt(i)); 

tempo di esecuzione: 3,6 ms

Sento che ho bisogno di estendere il mio commento un po ‘…

Informazioni su paradigm \ style

Questo è probabilmente l’aspetto più notevole. FP è diventato popolare a causa di ciò che si può ottenere evitando effetti collaterali. Non approfondirò i vantaggi che si possono trarre da questo, poiché questo non è correlato alla domanda.

Tuttavia, dirò che l’iterazione usando Iterable.forEach è ispirata da FP e piuttosto il risultato di portare più FP in Java (ironicamente, direi che non c’è molto uso per forEach in puro FP, dal momento che non fa nulla se non introducendo effetti collaterali).

Alla fine direi che è piuttosto una questione di gusto \ stile \ paradigma in cui stai scrivendo.

A proposito di parallelismo.

Dal punto di vista delle prestazioni non ci sono benefici notabili promessi dall’uso di Iterable.forEach over foreach (…).

Secondo i documenti ufficiali su Iterable.forEach :

Esegue l’azione specificata sui contenuti di Iterable, negli elementi di ordine che si verificano durante l’iterazione, fino a quando tutti gli elementi sono stati elaborati o l’azione genera un’eccezione.

… cioè doc molto chiaro che non ci sarà alcun parallelismo implicito. L’aggiunta di una sarebbe la violazione di LSP.

Ora, ci sono “collezioni parallell” che sono promesse in Java 8, ma per lavorare con quelle che mi sono più necessarie e mettere un po ‘più attenzione per usarle (vedi la risposta di mschenk74 per esempio).

BTW: in questo caso verrà utilizzato Stream.forEach e non garantisce che il lavoro effettivo verrà eseguito in parallelo (dipende dalla raccolta sottostante).

AGGIORNAMENTO: potrebbe non essere così ovvio e un po ‘teso a colpo d’occhio, ma c’è un altro aspetto della prospettiva di stile e leggibilità.

Prima di tutto – i vecchi passeri solitari sono semplici e vecchi. Tutti li conoscono già.

In secondo luogo, e più importante – probabilmente si desidera utilizzare Iterable.forEach solo con lambdas one-liner. Se il “corpo” diventa più pesante, tendono a non essere leggibile. Hai 2 opzioni da qui – usa le classi interne (schifo) o usa il vecchio forloop. Le persone spesso si infastidiscono quando vedono le stesse cose (iteratins rispetto alle collezioni) che vengono eseguite vari vays / styles nella stessa base di codice, e questo sembra essere il caso.

Ancora una volta, questo potrebbe o potrebbe non essere un problema. Dipende dalle persone che lavorano sul codice.

Uno dei più funzionali per i limiti di forEach è la mancanza di supporto per le eccezioni controllate.

Una soluzione ansible è sostituire il terminale forEach con il vecchio ciclo foreach normale:

  Stream stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty()); Iterable iterable = stream::iterator; for (String s : iterable) { fileWriter.append(s); } 

Ecco l’elenco delle domande più frequenti con altre soluzioni alternative per la gestione delle eccezioni controllate all’interno di lambdas e stream:

Funzione Java 8 Lambda che genera un’eccezione?

Java 8: Lambda-Stream, Filtro per metodo con eccezione

Come posso lanciare le eccezioni CONTROLLATE dagli stream di Java 8?

Java 8: gestione delle eccezioni controllate obbligatorie nelle espressioni lambda. Perché obbligatorio, non opzionale?

Il vantaggio del metodo Java 1.8 forOne su 1.7 Enhanced for loop è che durante la scrittura del codice è ansible concentrarsi solo sulla business logic.

Il metodo forOach accetta l’argomento java.util.function.Consumer come argomento, quindi aiuta ad avere la nostra logica di business in una posizione separata che è ansible riutilizzarla in qualsiasi momento.

Dai un’occhiata al seguente snippet,

  • Qui ho creato una nuova class che sovrascriverà il metodo di class accept da Consumer Class, in cui è ansible aggiungere funzionalità aggiuntive, More than Iteration .. !!!!!!

     class MyConsumer implements Consumer{ @Override public void accept(Integer o) { System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o); } } public class ForEachConsumer { public static void main(String[] args) { // Creating simple ArrayList. ArrayList aList = new ArrayList<>(); for(int i=1;i<=10;i++) aList.add(i); //Calling forEach with customized Iterator. MyConsumer consumer = new MyConsumer(); aList.forEach(consumer); // Using Lambda Expression for Consumer. (Functional Interface) Consumer lambda = (Integer o) ->{ System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o); }; aList.forEach(lambda); // Using Anonymous Inner Class. aList.forEach(new Consumer(){ @Override public void accept(Integer o) { System.out.println("Calling with Anonymous Inner Class "+o); } }); } } 

Here are some excerpts from the famous book, Effective Java (Third Edition) (Items 45 and 46). Indeed I would replace every loop of my code with forEach operation until I read these:

Overusing streams makes programs hard to read and maintain
When you start using streams, you may feel the urge to convert all your loops into streams, but resist the urge. While it may be possible, it will likely harm the readability and maintainability of your code base.

[…] The problem stems from the fact that this code is doing all its work in a terminal forEach operation, using a lambda that mutates external state […]. A forEach operation that does anything more than present the result of the computation performsd by a stream is a “bad smell in code,” as is a lambda that mutates state.

Java programmers know how to use for-each loops, and the forEach terminal operation is similar. But the forEach operation is among the least powerful of the terminal operations and the least stream-friendly. It’s explicitly iterative, and hence not amenable to parallelization. The forEach operation should be used only to report the result of a stream computation, not to perform the computation. Occasionally, it makes sense to use forEach for some other purpose, such as adding the results of a stream computation to a preexisting collection.