Qual è la differenza tra Collection.stream (). ForEach () e Collection.forEach ()?


287

Capisco che con .stream(), posso usare operazioni a catena come .filter()o usare il flusso parallelo. Ma qual è la differenza tra loro se devo eseguire piccole operazioni (ad esempio, stampare gli elementi dell'elenco)?

collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);

Risposte:


288

Per casi semplici come quello illustrato, sono per lo più gli stessi. Tuttavia, ci sono una serie di sottili differenze che potrebbero essere significative.

Un problema riguarda l'ordinazione. Con Stream.forEach, l'ordine non è definito . È improbabile che si verifichi con flussi sequenziali, tuttavia, rientra nelle specifiche per Stream.forEachl'esecuzione in un ordine arbitrario. Ciò si verifica frequentemente in flussi paralleli. Al contrario, Iterable.forEachviene sempre eseguito nell'ordine di iterazione di Iterable, se specificato.

Un altro problema riguarda gli effetti collaterali. L'azione specificata in Stream.forEachdeve essere non interferente . (Vedi il documento del pacchetto java.util.stream ) Iterable.forEachpotenzialmente ha meno restrizioni. Per le raccolte in java.util, Iterable.forEachverranno generalmente utilizzate quelle raccolte Iterator, la maggior parte delle quali sono progettate per essere a prova di errore e che verranno generate ConcurrentModificationExceptionse la raccolta viene modificata strutturalmente durante l'iterazione. Tuttavia, durante l'iterazione sono consentite modifiche non strutturali . Ad esempio, la documentazione della classe ArrayList afferma che "la semplice impostazione del valore di un elemento non è una modifica strutturale". Quindi, l'azione perArrayList.forEachè consentito impostare valori nel sottostante ArrayListsenza problemi.

Le raccolte concorrenti sono ancora diverse. Invece di fail-fast, sono progettati per essere debolmente coerenti . La definizione completa è a quel link. In breve, però, considera ConcurrentLinkedDeque. L'azione passata al suo forEachmetodo è autorizzata a modificare la deque sottostante, anche strutturalmente, e ConcurrentModificationExceptionnon viene mai lanciata. Tuttavia, la modifica che si verifica potrebbe o meno essere visibile in questa iterazione. (Da qui la consistenza "debole".)

Ancora un'altra differenza è visibile se Iterable.forEachsta ripetendo una raccolta sincronizzata. In una tale raccolta, Iterable.forEach prende il blocco della raccolta una volta e lo tiene su tutte le chiamate al metodo action. La Stream.forEachchiamata utilizza il divisore della raccolta, che non si blocca e che si basa sulla regola prevalente di non interferenza. La raccolta che supporta lo stream potrebbe essere modificata durante l'iterazione e, in caso ConcurrentModificationExceptionaffermativo, potrebbe risultare un comportamento incoerente.


Iterable.forEach takes the collection's lock. Da dove provengono queste informazioni? Non riesco a trovare un simile comportamento nelle fonti JDK.
turbante


@Stuart, puoi approfondire senza interferire. Stream.forEach () genererà anche ConcurrentModificationException (almeno per me).
yuranos,

1
@ yuranos87 Molte raccolte, come quelle, ArrayListhanno un controllo abbastanza rigoroso per le modifiche simultanee, e quindi spesso vengono lanciate ConcurrentModificationException. Ma questo non è garantito, in particolare per i flussi paralleli. Invece di CME potresti ricevere una risposta inaspettata. Considera anche le modifiche non strutturali alla sorgente del flusso. Per i flussi paralleli, non sai quale thread elaborerà un particolare elemento, né se è stato elaborato al momento della modifica. Questo imposta una condizione di gara, in cui potresti ottenere risultati diversi su ogni corsa e mai ottenere un CME.
Stuart Marks

30

Questa risposta riguarda le prestazioni delle varie implementazioni dei loop. È solo marginalmente rilevante per i loop chiamati MOLTO SPESSO (come milioni di chiamate). Nella maggior parte dei casi il contenuto del loop sarà di gran lunga l'elemento più costoso. Per le situazioni in cui esegui un ciclo molto spesso, questo potrebbe essere ancora interessante.

È necessario ripetere questi test nel sistema di destinazione in quanto è specifico dell'implementazione ( codice sorgente completo ).

Corro openjdk versione 1.8.0_111 su una macchina Linux veloce.

Ho scritto un test che scorre 10 ^ 6 volte su un elenco usando questo codice con dimensioni variabili per integers(10 ^ 0 -> 10 ^ 5 voci).

I risultati sono di seguito, il metodo più veloce varia a seconda della quantità di voci nell'elenco.

Ma ancora nelle situazioni peggiori, il looping di 10 ^ 5 voci 10 ^ 6 volte ha richiesto 100 secondi per il peggior performer, quindi altre considerazioni sono più importanti praticamente in tutte le situazioni.

public int outside = 0;

private void forCounter(List<Integer> integers) {
    for(int ii = 0; ii < integers.size(); ii++) {
        Integer next = integers.get(ii);
        outside = next*next;
    }
}

private void forEach(List<Integer> integers) {
    for(Integer next : integers) {
        outside = next * next;
    }
}

private void iteratorForEach(List<Integer> integers) {
    integers.forEach((ii) -> {
        outside = ii*ii;
    });
}
private void iteratorStream(List<Integer> integers) {
    integers.stream().forEach((ii) -> {
        outside = ii*ii;
    });
}

Ecco i miei tempi: millisecondi / funzione / numero di voci nell'elenco. Ogni corsa è di 10 ^ 6 loop.

                           1    10    100    1000    10000
       iterator.forEach   27   116    959    8832    88958
               for:each   53   171   1262   11164   111005
         for with index   39   112    920    8577    89212
iterable.stream.forEach  255   324   1030    8519    88419

Se ripeti l'esperimento, ho pubblicato il codice sorgente completo . Modifica questa risposta e aggiungi i risultati con una notazione del sistema testato.


Utilizzando un MacBook Pro, Intel Core i7 a 2,5 GHz, 16 GB, macOS 10.12.6:

                           1    10    100    1000    10000
       iterator.forEach   27   106   1047    8516    88044
               for:each   46   143   1182   10548   101925
         for with index   49   145    887    7614    81130
iterable.stream.forEach  393   397   1108    8908    88361

Java 8 Hotspot VM - Intel Xeon da 3,4 GHz, 8 GB, Windows 10 Pro

                            1    10    100    1000    10000
        iterator.forEach   30   115    928    8384    85911
                for:each   40   125   1166   10804   108006
          for with index   30   120    956    8247    81116
 iterable.stream.forEach  260   237   1020    8401    84883

Hotspot VM Java 11 - Intel Xeon da 3,4 GHz, 8 GB, Windows 10 Pro
(stessa macchina di cui sopra, versione JDK diversa)

                            1    10    100    1000    10000
        iterator.forEach   20   104    940    8350    88918
                for:each   50   140    991    8497    89873
          for with index   37   140    945    8646    90402
 iterable.stream.forEach  200   270   1054    8558    87449

Java 11 OpenJ9 VM - Intel Xeon da 3,4 GHz, 8 GB, Windows 10 Pro
(stessa macchina e versione JDK di cui sopra, VM diversa)

                            1    10    100    1000    10000
        iterator.forEach  211   475   3499   33631   336108
                for:each  200   375   2793   27249   272590
          for with index  384   467   2718   26036   261408
 iterable.stream.forEach  515   714   3096   26320   262786

Java 8 Hotspot VM - 2,8 GHz AMD, 64 GB, Windows Server 2016

                            1    10    100    1000    10000
        iterator.forEach   95   192   2076   19269   198519
                for:each  157   224   2492   25466   248494
          for with index  140   368   2084   22294   207092
 iterable.stream.forEach  946   687   2206   21697   238457

Java 11 Hotspot VM - 2,8 GHz AMD, 64 GB, Windows Server 2016
(stessa macchina come sopra, versione JDK diversa)

                            1    10    100    1000    10000
        iterator.forEach   72   269   1972   23157   229445
                for:each  192   376   2114   24389   233544
          for with index  165   424   2123   20853   220356
 iterable.stream.forEach  921   660   2194   23840   204817

Java 11 OpenJ9 VM - 2.8 GHz AMD, 64 GB, Windows Server 2016
(stessa macchina e versione JDK di cui sopra, VM diversa)

                            1    10    100    1000    10000
        iterator.forEach  592   914   7232   59062   529497
                for:each  477  1576  14706  129724  1190001
          for with index  893   838   7265   74045   842927
 iterable.stream.forEach 1359  1782  11869  104427   958584

L'implementazione della VM scelta fa anche la differenza Hotspot / OpenJ9 / ecc.


3
Questa è una risposta molto bella, grazie! Ma dal primo sguardo (e anche dal secondo) non è chiaro quale metodo corrisponda a quale esperimento.
torina,

Sento che questa risposta richiede più voti positivi per il test del codice :).
Cory,

per gli esempi di test +1
Centos,

8

Non c'è alcuna differenza tra i due che hai citato, almeno concettualmente, il Collection.forEach() è solo una scorciatoia.

Internamente il stream() versione ha un po 'più di sovraccarico a causa della creazione di oggetti, ma guardando il tempo di esecuzione non ha nemmeno un sovraccarico lì.

Entrambe le implementazioni finiscono per iterare sui collectioncontenuti una volta e durante l'iterazione stampano l'elemento.


Il sovraccarico di creazione dell'oggetto che menzioni, ti riferisci alla Streamcreazione o ai singoli oggetti? AFAIK, a Streamnon duplica gli elementi.
Raffi Khatchadourian,

30
Questa risposta sembra contraddire l'eccellente risposta scritta dal signore che sviluppa le librerie di base Java presso Oracle Corporation.
Dawood ibn Kareem,

0

Collection.forEach () utilizza l'iteratore della raccolta (se specificato). Ciò significa che è definito l'ordine di elaborazione degli articoli. Al contrario, l'ordine di elaborazione di Collection.stream (). ForEach () non è definito.

Nella maggior parte dei casi, non fa differenza quale delle due scegliamo. I flussi paralleli ci consentono di eseguire il flusso in più thread e, in tali situazioni, l'ordine di esecuzione non è definito. Java richiede solo il completamento di tutti i thread prima di chiamare qualsiasi operazione terminale, come Collectors.toList (). Vediamo un esempio in cui prima chiamiamo forEach () direttamente sulla raccolta e, in secondo luogo, su un flusso parallelo:

list.forEach(System.out::print);
System.out.print(" ");
list.parallelStream().forEach(System.out::print);

Se eseguiamo il codice più volte, vediamo che list.forEach () elabora gli elementi in ordine di inserimento, mentre list.parallelStream (). ForEach () produce un risultato diverso ad ogni esecuzione. Un possibile output è:

ABCD CDBA

Un altro è:

ABCD DBCA
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.