Java 8 iterabile.forEach () vs foreach ciclo


Quale delle seguenti è una migliore pratica 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 for che potrebbero essere "semplificati" con lambda, ma c'è davvero qualche vantaggio nell'usarli? Migliorerebbe le loro prestazioni e leggibilità?

MODIFICA

Estenderò anche questa domanda a metodi più lunghi. So che non è possibile restituire o interrompere la funzione genitore da un lambda e anche questo dovrebbe essere preso in considerazione quando li si confronta, ma c'è qualcos'altro da considerare?

Author: Rafael, 2013-05-19

8 answers

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 vantaggio principale dal mio punto di vista è che l'implementazione di ciò che deve essere fatto all'interno del ciclo può essere definita senza dover decidere se verrà eseguita in parallelo o sequenziale

  • Se vuoi che il tuo ciclo venga eseguito in parallelamente potresti semplicemente scrivere

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

    Dovrai scrivere del codice extra per la gestione del thread, ecc.

Nota: Per la mia risposta ho assunto join che implementano l'interfaccia java.util.Stream. Se joins implementa solo l'interfaccia java.util.Iterable questo non è più vero.

 157
Author: mschenk74, 2018-06-29 07:40:06

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

  • Impossibile utilizzare le variabili non finali. 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;
    }
    
  • Impossibile gestire le eccezioni controllate. I lambda non sono in realtà vietati dal lancio di eccezioni controllate, ma funzionali comuni interfacce come Consumer non ne dichiarano alcuna. Pertanto, qualsiasi codice che genera eccezioni controllate deve avvolgerle in try-catch o Throwables.propagate(). Ma anche se lo fai, non è sempre chiaro cosa succede all'eccezione lanciata. Potrebbe essere inghiottito da qualche parte nelle viscere di forEach()

  • Controllo di flusso limitato. Un return in un lambda è uguale a un continue in un for-each, ma non esiste un equivalente a un break. È anche difficile fare cose come valori di ritorno, cortocircuito o set flags (che avrebbe alleviato un po ' le cose, se non fosse stata una violazione della regola nessuna variabile non finale). "Questa non è solo un'ottimizzazione, ma fondamentale se si considera che alcune sequenze (come leggere le righe in un file) possono avere effetti collaterali, o si può avere una sequenza infinita."

  • Potrebbe essere eseguito in parallelo, che è una cosa orribile e orribile per tutti tranne lo 0,1% del tuo codice che deve essere ottimizzato. Qualsiasi parallelo il codice deve essere pensato (anche se non utilizza blocchi, 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 semplici, specialmente ora che i lambda sono nuovi. Per "ottimizzazione" non intendo il sovraccarico di chiamare lambda (che è piccolo), ma l'analisi e la trasformazione sofisticate che il moderno compilatore JIT esegue sul codice in esecuzione.

  • Se hai bisogno di parallelismo, è probabilmente molto più veloce e non molto più difficile usare un ExecutorService. I flussi sono entrambi automagici (leggi: non so molto del tuo problema) e utilizzano una strategia di parallelizzazione specializzata (leggi: inefficiente per il caso generale) (decomposizione ricorsiva fork-join).

  • Rende il debug più confuso, perché della gerarchia delle chiamate annidate e, dio non voglia, l'esecuzione parallela. Il debugger potrebbe avere problemi a visualizzare le variabili dal codice circostante e cose come step-through potrebbero non funzionare come previsto.

  • I flussi in generale sono più difficili da codificare, leggere ed eseguire il debug. In realtà, questo è vero per le API complesse "fluenti" in generale. La combinazione di dichiarazioni singole complesse, l'uso pesante di generici e la mancanza di variabili intermedie cospirano per produrre confondere i messaggi di errore e vanificare il debug. Invece di" questo metodo non ha un sovraccarico per il tipo X "si ottiene un messaggio di errore più vicino a" da qualche parte hai incasinato i tipi, ma non sappiamo dove o come."Allo stesso modo, non è possibile 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, la lettura del codice e la comprensione dei tipi e del comportamento in ogni fase dell'esecuzione possono essere non banale.

  • Sporge come un pollice dolorante. Il linguaggio Java ha già l'istruzione for-each. Perché sostituirlo con una chiamata di funzione? Perché incoraggiare a nascondere gli effetti collaterali da qualche parte nelle espressioni? Perché incoraggiare ingombranti one-liners? Miscelazione regolare per-ogni e nuovo forEach volenti o nolenti è cattivo stile. Il codice dovrebbe parlare in idiomi (modelli che sono veloci da comprendere a causa della loro ripetizione), e meno idiomi vengono usati più chiaro è il codice e meno tempo è speso decidere quale idioma usare (un grande tempo-scarico 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 il metodo iterator) e non può essere usato in un for-each, solo con un forEach(). Raccomando di lanciare flussi in iterabili con (Iterable<T>)stream::iterator. Un'alternativa migliore è usare StreamEx che risolve una serie di problemi di API di flusso, inclusa l'implementazione di Iterable.

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

  • Atomicamente iterazione su un elenco sincronizzato. 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 flusso parallelo appropriato). Ciò consente di risparmiare alcune righe di codice rispetto all'utilizzo di un ExecutorService, se il problema corrisponde alle ipotesi di prestazioni incorporate in Stream e Spliterator.

  • Contenitori specifici che, come l'elenco sincronizzato, beneficiano di avere il controllo dell'iterazione (anche se questo è in gran parte teorico a meno che le persone non possano far apparire più esempi)

  • Chiamare una singola funzione in modo più pulito usando forEach() e un argomento di riferimento del metodo (cioè list.forEach (obj::someMethod)). Tuttavia, tenere a mente i punti su eccezioni controllate, debug più difficile e riduzione del numero di idiomi utilizzati durante la scrittura del codice.

Articoli che ho usato per riferimento:

MODIFICA: Assomiglia ad alcune delle proposte originali per lambda (come http://www.javac.info/closures-v06a.html ) risolto alcuni dei problemi che ho menzionato (aggiungendo le proprie complicazioni, ovviamente).

 401
Author: Aleksandr Dubinsky, 2017-04-12 07:32:04

Leggendo questa domanda si può avere l'impressione che Iterable#forEach in combinazione con le espressioni lambda sia una scorciatoia/sostituzione per scrivere un ciclo tradizionale per ogni ciclo. Questo semplicemente non è vero. Questo codice del PO:

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

È non inteso come scorciatoia per scrivere

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

E non dovrebbe certamente essere usato in questo modo. Invece è inteso come una scorciatoia (sebbene sia non esattamente lo stesso) per scrivere

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

Ed è in sostituzione del seguente codice Java 7:

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

Sostituendo il corpo di un ciclo con una interfaccia funzionale, come negli esempi di cui sopra, rende il codice più esplicito: stai dicendo che (1) il corpo del ciclo non influisce circostanti codice e il controllo di flusso, e (2) il corpo del ciclo può essere sostituita con una diversa implementazione della funzione, senza compromettere circostanti codice. Non essere in grado di accedere a variabili non finali dell'ambito 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 sintassi di Iterable#forEach, rende il codice più leggibile, perché si ottengono immediatamente queste informazioni aggiuntive sul codice.

I loop tradizionali per ogni ciclo rimarranno sicuramente buone pratiche (per evitare il termine abusato " buone pratiche ") in Java. Ma questo non significa che Iterable#forEach dovrebbe essere considerato cattiva pratica o cattivo stile. È sempre buona pratica, utilizzare lo strumento giusto per fare il lavoro, e questo include la miscelazione tradizionale per ogni loop con Iterable#forEach, dove ha senso.

Poiché gli aspetti negativi di Iterable#forEach sono già stati discussi in questa discussione, ecco alcuni motivi, perché probabilmente potresti voler usare Iterable#forEach:

  • Per rendere il tuo codice più esplicito: Come descritto sopra, Iterable#forEach può rendere il tuo codice più esplicito e leggibile in alcuni situazione.

  • Per rendere il codice più estensibile e gestibile: L'utilizzo di una funzione come corpo di un ciclo consente di sostituire questa funzione con diverse implementazioni (vedere Strategy Pattern ). Ad esempio, è possibile sostituire facilmente l'espressione lambda con una chiamata al metodo, che può essere sovrascritta dalle sottoclassi:

    joins.forEach(getJoinStrategy());
    

    Quindi è possibile fornire strategie predefinite utilizzando un enum, che implementa l'interfaccia funzionale. Questo non solo rende il vostro codice più estensibile, 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 il debug più semplice, 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 di debug potrebbe ad esempio essere un delegato , che decora l'effettiva implementazione della funzione.

  • Per ottimizzare il codice critico per le prestazioni: Contrariamente ad alcune 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 della libreria vorranno certamente valutare se alcune delle loro implementazioni di loop esistenti debbano essere sostituite con Iterable#forEach.

    Per sostenere questa affermazione con i fatti, ho fatto alcuni micro-benchmark con Pinza. Ecco il codice di test (è necessaria l'ultima pinza di git):

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> 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<TestObject>() {
                    @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++;
                }
            }
        }
    }
    

    Ed ecco i risultati:

    Quando in esecuzione con "- client", Iterable#forEach supera il tradizionale ciclo for su un ArrayList, ma è ancora più lento dell'iterazione diretta su un array. Quando si esegue con "- server", le prestazioni di tutti gli approcci sono circa le stesse.

  • Per fornire supporto opzionale per l'esecuzione parallela: È già stato detto qui, che la possibilità di eseguire l'interfaccia funzionale di Iterable#forEachin parallelo utilizzando flussi, è certamente un aspetto importante. Poiché Collection#parallelStream() non garantisce che il ciclo sia effettivamente eseguito in parallelo, bisogna considerare questa funzione opzionale. Iterando sulla tua lista con list.parallelStream().forEach(...);, dici esplicitamente: Questo ciclo supporta l'esecuzione parallela, ma non dipende da esso. Ancora una volta, questa è una caratteristica e non un deficit!

    Spostando la decisione per l'esecuzione parallela dall'implementazione del ciclo effettivo, si consente l'ottimizzazione facoltativa del codice, senza influire sul codice stesso, che è una buona cosa. Inoltre, se l'implementazione del flusso parallelo predefinito non si adatta alle tue esigenze, nessuno ti impedisce di fornire la tua implementazione. Ad esempio, è possibile 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<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> 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 qui è che la tua implementazione del ciclo non ha bisogno di sapere o preoccuparsi di questi estremi.

 94
Author: Balder, 2015-01-13 16:58:46

forEach() può essere implementato per essere più veloce di for-each loop, perché 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<size; i++)
    action.accept(elements[i])

Al contrario del ciclo for-each che richiede un sacco di ponteggi

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 facendo l'oggetto 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.

 11
Author: ZhongYu, 2013-07-11 16:01:45

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

Ho sentito che avrei dovuto aggiungere i miei risultati dall'iterazione di benchmarking. Ho adottato un approccio molto semplice (nessun framework di benchmarking) e ho confrontato 5 metodi diversi:

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

La procedura di prova e i parametri

private List<Integer> 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 in questa classe deve essere ripetuto e avere alcuni doIt(Integer i) applicati a tutti si tratta di membri, ogni volta tramite un metodo diverso. nella classe principale eseguo il metodo testato tre volte per riscaldare la JVM. Quindi eseguo il metodo di test 1000 volte sommando il tempo necessario per ogni metodo di iterazione (usando System.nanoTime()). Dopo che è fatto divido quella somma 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 a 4 core, con java versione 1.8.0_05

Classico for

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

Tempo di esecuzione: 4.21 ms

Classico foreach

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

 8
Author: Assaf, 2014-09-25 05:16:03

Sento che ho bisogno di estendere un po ' il mio commento...

Informazioni su paradigm\style

Questo è probabilmente l'aspetto più notevole. FP è diventato popolare a causa di ciò che si può ottenere evitando gli effetti collaterali. Non approfondirò in profondità quali pro \ contro puoi ottenere da questo, poiché questo non è correlato alla domanda.

Tuttavia, dirò che l'iterazione usando Iterable.forEach è ispirato 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 altro che introdurre 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 vantaggi notevoli promessi dall'utilizzo di Iterable.forEach su foreach(...).

Secondo i documenti ufficiali su Iterable.forEach :

Esegue l'azione specificata sul contenuto dell'iterabile, nel gli elementi dell'ordine si verificano durante l'iterazione, fino a quando tutti gli elementi elaborato o l'azione genera un'eccezione.

... cioè i documenti sono abbastanza chiari che non ci sarà alcun parallelismo implicito. L'aggiunta di uno sarebbe una violazione LSP.

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

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

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

Prima di tutto - plain old forloops sono semplici e vecchi. Tutti li conoscono già.

Secondo, e più importante-probabilmente vuoi usa Iterable.forEach solo con lambda one-liner. Se "corpo" diventa più pesante-tendono ad essere non-che leggibile. Hai 2 opzioni da qui: usa le classi interne (bleah) o usa il vecchio forloop. Le persone spesso si infastidiscono quando vedono le stesse cose (iteratine sulle collezioni) che vengono fatte varie vays/stili nella stessa base di codice, e questo sembra essere il caso.

Di nuovo, questo potrebbe o non potrebbe essere un problema. Dipende dalle persone che lavorano sul codice.

 3
Author: Eugene Loy, 2013-05-19 18:59:55

Una delle limitazioni funzionali più esaltanti di forEach è la mancanza del supporto delle eccezioni controllate.

Una possibile soluzione consiste nel sostituire il terminale forEach con un semplice vecchio ciclo foreach:

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

Ecco l'elenco delle domande più popolari con altre soluzioni alternative sulla gestione delle eccezioni controllate all'interno di lambda e stream:

Java 8 Funzione Lambda che genera eccezione?

Java 8: Lambda-Streams, Filtra per metodo con Eccezione

Come posso lanciare eccezioni CONTROLLATE dall'interno dei flussi Java 8?

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

 2
Author: Vadzim, 2017-05-23 11:33:26

Il vantaggio del metodo Java 1.8 forEach su 1.7 Enhanced for loop è che durante la scrittura del codice è possibile concentrarsi solo sulla logica di business.

Il metodo ForEach prende java.util.funzione.Oggetto Consumer come argomento, quindi Aiuta ad avere la nostra logica di business in una posizione separata che puoi riutilizzare in qualsiasi momento.

Guarda sotto snippet,

  • Qui ho creato una nuova classe che sovrascriverà il metodo accept class dalla classe Consumer, cui è possibile aggiungere funzionalità aggiuntive, più dell'iterazione..!!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @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<Integer> 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<Integer> 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<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
    
 1
Author: Hardik Patel, 2017-12-23 14:59:03