Java 8 Iterable.forEach () vs foreach loop


466

Quale delle seguenti è la 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 un sacco di loop che potrebbero essere "semplificati" con lambdas, ma c'è davvero qualche vantaggio nell'usarli? Migliorerebbe le loro prestazioni e leggibilità?

MODIFICARE

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


10
Non c'è un reale vantaggio prestazionale l'uno rispetto all'altro. La prima opzione è qualcosa ispirata a FP (di cui si parla comunemente come un modo più "carino" e "chiaro" per esprimere il proprio codice). In realtà - questa è piuttosto una domanda "di stile".
Eugene Loy,

5
@Dwb: in questo caso, non è rilevante. forEach non è definito come parallelo o qualcosa del genere, quindi queste due cose sono semanticamente equivalenti. Naturalmente è possibile implementare una versione parallela di forEach (e uno potrebbe già essere presente nella libreria standard), e in tal caso la sintassi dell'espressione lambda sarebbe molto utile.
AardvarkSoup

8
@AardvarkSoup L'istanza su cui viene chiamato forEach è uno Stream ( lambdadoc.net/api/java/util/stream/Stream.html ). Per richiedere un'esecuzione parallela si potrebbe scrivere joins.parallel (). ForEach (...)
mschenk74

13
È joins.forEach((join) -> mIrc.join(mSession, join));davvero una "semplificazione" di for (String join : joins) { mIrc.join(mSession, join); }? Hai aumentato il numero di punteggiatura da 9 a 12, a vantaggio di nascondere il tipo di join. Quello che hai davvero fatto è mettere due affermazioni su una riga.
Tom Hawtin - tackline il

7
Un altro punto da considerare è la capacità di acquisizione variabile variabile di Java. Con Stream.forEach (), non puoi aggiornare le variabili locali poiché la loro acquisizione le rende definitive, il che significa che puoi avere un comportamento con stato in forEach lambda (a meno che tu non sia preparato per un po 'di bruttezza come l'uso di variabili di stato di classe).
RedGlyph

Risposte:


512

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

  • Impossibile utilizzare le variabili non finali . Quindi, codice come il seguente non può essere trasformato in un forda Ogni lambda:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • Impossibile gestire le eccezioni verificate . A Lambdas non è effettivamente proibito lanciare eccezioni verificate, ma le interfacce funzionali comuni come Consumernon ne dichiarano alcuna. Pertanto, qualsiasi codice che genera eccezioni controllate deve includerli in try-catcho Throwables.propagate(). Ma anche se lo fai, non è sempre chiaro cosa succede all'eccezione generata. Potrebbe essere ingoiato da qualche parte nelle viscere diforEach()

  • Controllo di flusso limitato . A returnin a lambda è uguale a a continuein a per-ciascuno, ma non esiste un equivalente di a break. È anche difficile fare cose come valori di ritorno, corto circuito o impostare flag (che avrebbe alleviato un po 'le cose, se non fosse stata una violazione della regola delle variabili non finali ). "Questa non è solo un'ottimizzazione, ma fondamentale quando 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, orribile per tutti tranne lo 0,1% del codice che deve essere ottimizzato. Qualsiasi codice parallelo deve essere considerato attraverso (anche se non utilizza blocchi, volatili e altri aspetti particolarmente cattivi dell'esecuzione multi-thread tradizionale). Qualsiasi bug sarà difficile da trovare.

  • Potrebbe danneggiare le prestazioni , perché JIT non può ottimizzare forEach () + lambda nella stessa misura dei semplici loop, soprattutto ora che i lambda sono nuovi. Per "ottimizzazione" non intendo il sovraccarico di chiamare lambdas (che è piccolo), ma per la sofisticata analisi e trasformazione 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 sia automagici (leggi: non sanno molto del tuo problema) sia usano una strategia specializzata (leggi: inefficiente per il caso generale) ( decomposizione ricorsiva fork-join ).

  • Rende il debug più confuso , a causa della gerarchia di chiamate nidificate e, a Dio non voglia, esecuzione parallela. Il debugger potrebbe avere problemi nella visualizzazione delle variabili dal codice circostante e cose come il passaggio rapido potrebbero non funzionare come previsto.

  • Gli stream in generale sono più difficili da codificare, leggere e debug . In realtà, questo è vero per le API complesse " fluenti " in generale. La combinazione di singole istruzioni complesse, un uso intenso di generici e la mancanza di variabili intermedie cospirano per produrre messaggi di errore confusi e debug frustrato. Invece di "questo metodo non ha un sovraccarico per il tipo X" viene visualizzato 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 scorrere ed esaminare le cose in un debugger con la stessa facilità di quando il codice viene suddiviso in più istruzioni e i valori intermedi vengono salvati in variabili. Infine, leggere il codice e comprendere i tipi e il comportamento in ogni fase dell'esecuzione potrebbe non essere 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 effetti collaterali da qualche parte nelle espressioni? Perché incoraggiare un liner monotono? Mescolare regolarmente per ognuno e nuovo per ogni volente o nolente è un cattivo stile. Il codice dovrebbe parlare in idiomi (schemi che sono rapidi da comprendere a causa della loro ripetizione), e meno idiomi vengono usati più chiaro è il codice e meno tempo viene speso a decidere quale idioma usare (un grande dispendio di tempo per i 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 Streamnon si attua Iterable(nonostante abbia effettivamente un metodo iterator) e non può essere utilizzato in un for-each, solo con un forEach (). Consiglio di trasmettere Stream in Iterables con (Iterable<T>)stream::iterator. Un'alternativa migliore è utilizzare StreamEx che risolve una serie di problemi relativi all'API Stream, inclusa l'implementazione Iterable.

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

  • Iterando atomicamente 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 sulle prestazioni integrate in Stream e Spliterator.

  • Contenitori specifici che , come l'elenco sincronizzato, traggono vantaggio dal controllo dell'iterazione (sebbene ciò sia in gran parte teorico a meno che le persone non possano far apparire altri esempi)

  • Chiamare una singola funzione in modo più pulito utilizzando forEach()e un argomento di riferimento del metodo (ovvero, list.forEach (obj::someMethod)). Tuttavia, tenere presente i punti relativi alle eccezioni verificate, al debug più difficile e alla riduzione del numero di idiomi utilizzati durante la scrittura del codice.

Articoli che ho usato come riferimento:

EDIT: Sembra che alcune delle proposte originali di lambdas (come http://www.javac.info/closures-v06a.html ) abbiano risolto alcuni dei problemi che ho citato (aggiungendo ovviamente le loro complicazioni).


95
"Perché incoraggiare a nascondere effetti collaterali da qualche parte nelle espressioni?" è la domanda sbagliata. Il funzionale forEachè lì per incoraggiare lo stile funzionale, cioè usando espressioni senza effetti collaterali. Se incontri la situazione, la forEachcosa non funziona bene con i tuoi effetti collaterali, dovresti avere la sensazione che non stai usando lo strumento giusto per il lavoro. Quindi la semplice risposta è, perché il tuo feeling è giusto, quindi stai al ciclo for-each per quello. Il forciclo classico non è diventato deprecato ...
Holger,

17
@Holger Come si può forEachusare senza effetti collaterali?
Aleksandr Dubinsky,

14
Va bene, non ero abbastanza preciso, forEachè l'unica operazione di streaming destinata agli effetti collaterali, ma non è per gli effetti collaterali come il tuo codice di esempio, il conteggio è un'operazione tipica reduce. Suggerirei, di regola, di mantenere tutte le operazioni che manipolano le variabili locali o influenzano il flusso di controllo (compresa la gestione delle eccezioni) in un forciclo classico . Per quanto riguarda la domanda originale, penso che il problema derivi dal fatto che qualcuno utilizza un flusso in cui forsarebbe sufficiente un semplice ciclo sulla sorgente del flusso. Usa uno stream dove forEach()funziona solo
Holger il

8
@Holger Qual è un esempio di effetti collaterali che forEachsarebbe appropriato?
Aleksandr Dubinsky,

29
Qualcosa che elabora ogni singolo elemento e non tenta di mutare le variabili locali. Ad esempio manipolando gli elementi stessi o stampandoli, scrivendoli / inviandoli a un file, a un flusso di rete, ecc. Non è un problema per me se metti in discussione questi esempi e non visualizzi alcuna applicazione; filtraggio, mappatura, riduzione, ricerca e (in misura minore) raccolta sono le operazioni preferite di un flusso. Ogni for mi sembra una comodità per il collegamento con le API esistenti. E per operazioni parallele, ovviamente. Questi non funzioneranno con i forloop.
Holger,

169

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 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 del codice aggiuntivo per la gestione dei thread, ecc.

Nota: per la mia risposta ho assunto l'adesione all'implementazione java.util.Streamdell'interfaccia. Se si unisce implementa solo l' java.util.Iterableinterfaccia di questo non è più vero.


4
Le diapositive di un ingegnere oracolare a cui si riferisce ( blogs.oracle.com/darcy/resource/Devoxx/… ) non menzionano il parallelismo all'interno di quelle espressioni lambda. Il parallelismo può verificarsi all'interno di metodi di raccolta di massa come mape foldche non sono realmente correlati alle lambda.
Thomas Jungblut,

1
Non sembra davvero che il codice di OP trarrà beneficio dal parallelismo automatico qui (soprattutto perché non vi è alcuna garanzia che ce ne sarà uno). Non sappiamo davvero cosa sia "mIrc", ma "join" non sembra davvero qualcosa che può essere esagerato.
Eugene Loy,

11
Stream#forEache Iterable#forEachnon sono la stessa cosa. OP sta chiedendo Iterable#forEach.
gvlasov,

2
Ho usato lo stile UPDATEX poiché ci sono stati cambiamenti nelle specifiche tra il momento in cui è stata posta la domanda e il momento in cui la risposta è stata aggiornata. Senza la storia della risposta, penso che sarebbe ancora più confuso.
mschenk74,

1
Qualcuno potrebbe spiegarmi perché questa risposta non è valida se si joinssta implementando Iterableanziché Stream? Da un paio di cose che ho letto, OP dovrebbe essere in grado di fare joins.stream().forEach((join) -> mIrc.join(mSession, join));e joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));se joinsimplementaIterable
Blueriver

112

Leggendo questa domanda si può avere l'impressione che, Iterable#forEachin combinazione con le espressioni lambda, sia una scorciatoia / sostituzione per scrivere un ciclo tradizionale per ogni. 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 non dovrebbe certamente essere usato in questo modo. Invece è inteso come un collegamento (anche se è non esattamente lo stesso) per la scrittura

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

Sostituire il corpo di un ciclo con un'interfaccia funzionale, come negli esempi sopra, rende il codice più esplicito: stai dicendo che (1) il corpo del ciclo non influenza il codice circostante e il flusso di controllo e (2) il il corpo del loop può essere sostituito con una diversa implementazione della funzione, senza influire sul codice circostante. 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 Iterable#forEachdalla semantica di un ciclo tradizionale per ogni. Una volta che ci si abitua alla sintassi di Iterable#forEach, rende il codice più leggibile, poiché si ottengono immediatamente queste informazioni aggiuntive sul codice.

I loop tradizionali per ogni rimarranno sicuramente buone pratiche (per evitare il termine abusato " best practice ") in Java. Ma questo non significa che Iterable#forEachdovrebbe essere considerata cattiva pratica o cattivo stile. È sempre una buona pratica usare lo strumento giusto per fare il lavoro, e ciò include la miscelazione di loop tradizionali per ciascuno di essi Iterable#forEach, dove ha senso.

Dato che gli aspetti negativi di Iterable#forEachsono già stati discussi in questo thread, ecco alcuni motivi, per cui potresti voler usare Iterable#forEach:

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

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

    joins.forEach(getJoinStrategy());

    Quindi è possibile 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 if(DEBUG)System.out.println(). L'implementazione del debug potrebbe essere ad esempio un delegato , che decora l'implementazione della funzione effettiva.

  • Per ottimizzare il codice critico per le prestazioni: Contrariamente ad alcune delle asserzioni in questo thread, Iterable#forEach fornisce già prestazioni migliori rispetto a un ciclo tradizionale per ogni, 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 queste prestazioni extra possono fare la differenza. Ad esempio, i manutentori delle librerie vorranno sicuramente valutare se alcune delle loro implementazioni di loop esistenti debbano essere sostituite con Iterable#forEach.

    A sostegno di 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<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++;
                }
            }
        }
    }
    

    E qui ci sono i risultati:

    Quando viene eseguito con "-client", Iterable#forEachsupera il tradizionale ciclo per un ArrayList, ma è ancora più lento rispetto all'iterazione diretta su un array. Quando si esegue con "-server", le prestazioni di tutti gli approcci sono più o meno le stesse.

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

    Allontanando la decisione per l'esecuzione parallela dall'implementazione del ciclo effettivo, consenti l'ottimizzazione facoltativa del codice, senza influire sul codice stesso, il che è positivo. Inoltre, se l'implementazione del flusso parallelo predefinito non soddisfa le tue esigenze, nessuno ti impedisce di fornire la tua implementazione. Ad esempio, è possibile fornire una raccolta ottimizzata in base al sistema operativo sottostante, alla dimensione 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 l'implementazione del tuo loop non ha bisogno di conoscere o preoccuparsi di questi dettagli.


5
Hai una visione interessante in questa discussione e sollevi una serie di punti. Proverò ad affrontarli. Si propone di alternare forEache for-eachbasarsi su alcuni criteri relativi alla natura del corpo del loop. La saggezza e la disciplina nel seguire tali regole sono il segno distintivo di un buon programmatore. Tali regole sono anche la sua rovina, perché le persone intorno a lui o non le seguono o non sono d'accordo. Ad esempio, utilizzando le eccezioni selezionate e non selezionate. Questa situazione sembra ancora più sfumata. Ma, se il corpo "non influenza il codice surround o il controllo del flusso", non viene considerato come una funzione migliore?
Aleksandr Dubinsky,

4
Grazie per i commenti dettagliati Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Sì, questo sarà spesso il caso secondo me: prendere in considerazione questi loop in quanto le funzioni sono una conseguenza naturale.
Balder,

2
Per quanto riguarda il problema delle prestazioni, suppongo che dipenda molto dalla natura del loop. In un progetto a cui sto lavorando, ho utilizzato loop in stile funzione simili a quelli Iterable#forEachprecedenti a Java 8 proprio a causa dell'aumento delle prestazioni. Il progetto in questione ha un loop principale simile a un loop di gioco, con un numero indefinito di loop secondari nidificati, in cui i client possono plug-in partecipanti al loop come funzioni. Una tale struttura software beneficia notevolmente Iteable#forEach.
Balder,

7
C'è una frase alla fine della mia critica: "Il codice dovrebbe parlare in idiomi, e meno idiomi vengono usati più chiaro è il codice e meno tempo viene speso per decidere quale idioma usare". Ho iniziato ad apprezzare profondamente questo punto quando sono passato da C # a Java.
Aleksandr Dubinsky,

7
Questa è una brutta discussione. Potresti usarlo per giustificare tutto quello che vuoi: perché non dovresti usare un ciclo for, perché un ciclo while è abbastanza buono e questo è un idioma in meno. Diamine, perché usare qualsiasi istruzione loop, switch o try / catch quando goto può fare tutto questo e altro.
Tapichu,

13

forEach()può essere implementato per essere più veloce di per ogni ciclo, perché l'iterabile 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 molte impalcature

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'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 il confronto di iterazioni interne / esterne per diversi casi d'uso.


8
perché l'iterabile conosce il modo migliore ma l'iteratore no?
mschenk74,

2
nessuna differenza essenziale, ma è necessario un codice aggiuntivo per conformarsi all'interfaccia iteratore, che può essere più costosa.
ZhongYu,

1
@ zhong.j.yu se si implementa Collection si implementa anche Iterable comunque. Quindi, non c'è sovraccarico di codice in termini di "aggiunta di altro codice per implementare metodi di interfaccia mancanti", se questo è il punto. Come diceva mschenk74, non sembra esserci alcun motivo per cui non sia possibile modificare il proprio iteratore per sapere come eseguire l'iterazione della propria raccolta nel miglior modo possibile. Sono d'accordo che potrebbe esserci un sovraccarico per la creazione di iteratori, ma sul serio, quelle cose di solito sono così economiche, che puoi dire che hanno un costo zero ...
Eugene Loy,

4
per esempio iterando un albero: void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}questo è più elegante dell'iterazione esterna e puoi decidere come sincronizzarlo al meglio
maniaco del cricchetto

1
Stranamente, l'unico commento nei String.joinmetodi (okay, join errato) è "Numero di elementi che probabilmente non vale il sovraccarico di Arrays.stream". quindi usano un posh per loop.
Tom Hawtin - tackline il

7

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 diversi metodi:

  1. classico for
  2. foreach classico
  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 doIt(Integer i)applicato alcuni a tutti i suoi membri, ogni volta con un metodo diverso. nella classe Main eseguo il metodo testato tre volte per riscaldare la JVM. Eseguo quindi il metodo di prova 1000 volte sommando il tempo necessario per ciascun metodo di iterazione (usando System.nanoTime()). Dopo aver fatto ciò 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);

Ho eseguito questo su una CPU i5 4 core, con la versione java 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

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


23
Come si ottengono quei numeri? Quale framework per benchmark stai usando? Se non stai usando nessuno e semplicemente System.out.printlnper visualizzare questi dati in modo ingenuo, tutti i risultati sono inutili.
Luiggi Mendoza,

2
Nessun quadro. Io uso System.nanoTime(). Se leggi la risposta vedrai come è stata fatta. Non penso che lo renda inutile visto che si tratta di una domanda relativa . Non mi importa quanto bene abbia funzionato un certo metodo, mi interessa quanto sia andato bene rispetto agli altri metodi.
Assaf,

31
E questo è lo scopo di un buon micro benchmark. Dal momento che non hai soddisfatto tali requisiti, i risultati sono inutili.
Luiggi Mendoza,

6
Posso consigliare di conoscere JMH invece, questo è ciò che viene utilizzato per Java stesso e fa un grande sforzo per ottenere i numeri corretti: openjdk.java.net/projects/code-tools/jmh
dsvensson

1
Sono d'accordo con @LuiggiMendoza. Non c'è modo di sapere che questi risultati sono coerenti o validi. Dio sa quanti benchmark ho fatto che continuano a riportare risultati diversi, soprattutto in base all'ordine di iterazione, alle dimensioni e cosa no.
mmm

6

Sento che devo estendere un po 'il mio commento ...

A proposito di paradigma \ stile

Questo è probabilmente l'aspetto più notevole. FP è diventato popolare grazie a ciò che puoi ottenere evitando gli effetti collaterali. Non approfondirò quali vantaggi puoi trarre da questo, dal momento che questo non è correlato alla domanda.

Tuttavia, dirò che l'iterazione che utilizza Iterable.forEach è ispirata a FP e piuttosto il risultato del portare più FP a Java (ironicamente, direi che non c'è molto uso per ogni in FP puro, poiché non fa altro che introdurre effetti collaterali).

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

Sul parallelismo.

Dal punto di vista delle prestazioni non ci sono notevoli vantaggi promessi dall'uso di Iterable.forOach over foreach (...).

Secondo i documenti ufficiali su Iterable.forEach :

Esegue l'azione data sul contenuto di Iterable, nell'ordine in cui gli elementi si verificano durante l'iterazione, fino a quando tutti gli elementi non sono stati elaborati o l'azione genera un'eccezione.

... vale a dire documenti abbastanza chiari che non ci sarà parallelismo implicito. Aggiungerne uno sarebbe una violazione di LSP.

Ora, ci sono "raccolte parallele" che sono promesse in Java 8, ma per lavorare con quelle che hai bisogno di me sono più esplicite e metti qualche cura in più per usarle (vedi la risposta di mschenk74 per esempio).

A proposito: 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 'allungato a colpo d'occhio ma c'è un'altra sfaccettatura di stile e prospettiva di leggibilità.

Prima di tutto - i vecchi vecchi pianura sono semplici e vecchi. Tutti li conoscono già.

In secondo luogo, e più importante, probabilmente vorrai usare Iterable.forOach solo con lambda da una fodera. Se il "corpo" diventa più pesante, tendono a non essere così leggibili. Hai 2 opzioni da qui: usa le classi interne (schifo) o usa semplicemente il vecchio forloop. Le persone spesso si arrabbiano quando vedono che le stesse cose (iteratine sulle raccolte) vengono eseguite su vari vays / stili nella stessa base di codice, e questo sembra essere il caso.

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


1
Il parallelismo non ha bisogno di nuove "collezioni parallele". Dipende solo dal fatto che tu abbia richiesto un flusso sequenziale (usando collection.stream ()) o uno parallelo (usando collection.parallelStream ()).
JB Nizet,

@JBNizet Secondo i documenti Collection.parallelStream () non garantisce che l'implementazione della raccolta restituirà un flusso parallelo. In realtà mi sto chiedendo quando questo potrebbe accadere, ma, probabilmente, questo dipende dalla raccolta.
Eugene Loy,

concordato. Dipende anche dalla collezione. Ma il mio punto era che i cicli foreach paralleli erano già disponibili con tutte le raccolte standard (ArrayList, ecc.). Non è necessario attendere "raccolte parallele".
JB Nizet,

@JBNizet è d'accordo sul tuo punto, ma non è proprio quello che intendevo per "collezioni parallele". Mi riferisco a Collection.parallelStream () che è stato aggiunto in Java 8 come "collezioni parallele" dall'analogia con il concetto di Scala che fa praticamente lo stesso. Inoltre, non sono sicuro di come venga chiamato nel bit di JSR. Ho visto un paio di articoli che usano la stessa terminologia per questa funzionalità di Java 8.
Eugene Loy,

1
per l'ultimo paragrafo è possibile utilizzare un riferimento di funzione:collection.forEach(MyClass::loopBody);
maniaco del cricchetto

6

Una delle forEachlimitazioni della maggior parte delle funzioni di upleasing è la mancanza di supporto per le eccezioni verificate.

Una possibile soluzione è quella di sostituire il terminale forEachcon 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 un elenco delle domande più comuni con altre soluzioni alternative sulla gestione delle eccezioni verificate all'interno di lambdas e stream:

Funzione Java 8 Lambda che genera un'eccezione?

Java 8: Lambda-Streams, filtro per metodo con eccezione

Come posso generare eccezioni CHECKED dagli stream Java 8?

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


3

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 aziendale.

Il metodo forEach prende l'oggetto java.util.function.Consumer come argomento, quindi aiuta ad avere la nostra logica aziendale in una posizione separata per poterla riutilizzare in qualsiasi momento.

Dai un'occhiata allo snippet di seguito,

  • Qui ho creato una nuova classe che sovrascriverà il metodo di classe da Consumer Class, dove è possibile aggiungere ulteriore funzionalità, Più che 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);
                }
            });
        }
    }
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.