¿Cuál es la diferencia entre Collection.stream (). ForEach () y Collection.forEach ()?


286

Entiendo que con .stream(), puedo usar operaciones en cadena como .filter()o usar flujo paralelo. Pero, ¿cuál es la diferencia entre ellos si necesito ejecutar pequeñas operaciones (por ejemplo, imprimir los elementos de la lista)?

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

Respuestas:


287

Para casos simples como el ilustrado, son en su mayoría los mismos. Sin embargo, hay una serie de diferencias sutiles que pueden ser significativas.

Un problema es con el pedido. Con Stream.forEach, el orden no está definido . Es poco probable que ocurra con secuencias secuenciales, aún así, está dentro de la especificación para Stream.forEachejecutarse en algún orden arbitrario. Esto ocurre con frecuencia en flujos paralelos. Por el contrario, Iterable.forEachsiempre se ejecuta en el orden de iteración delIterable , si se especifica uno.

Otro problema es con los efectos secundarios. Se Stream.forEachrequiere que la acción especificada en no interfiera . (Consulte el documento del paquete java.util.stream ). Iterable.forEachPotencialmente tiene menos restricciones. Para las colecciones en java.util, Iterable.forEachgeneralmente se usará esa colección Iterator, la mayoría de las cuales están diseñadas para ser a prueba de fallas y que se lanzarán ConcurrentModificationExceptionsi la colección se modifica estructuralmente durante la iteración. Sin embargo, las modificaciones que no son estructurales están permitidas durante la iteración. Por ejemplo, la documentación de la clase ArrayList dice que "simplemente establecer el valor de un elemento no es una modificación estructural". Por lo tanto, la acción paraArrayList.forEach puede establecer valores en el subyacente ArrayListsin problemas.

Las colecciones concurrentes son una vez más diferentes. En lugar de fallar rápidamente, están diseñados para ser débilmente consistentes . La definición completa está en ese enlace. Brevemente, sin embargo, considere ConcurrentLinkedDeque. La acción pasó a su forEachmétodo está permitido modificar el deque subyacente, incluso estructuralmente, y ConcurrentModificationExceptionno se lanza. Sin embargo, la modificación que se produce puede o no ser visible en esta iteración. (De ahí la consistencia "débil").

Aún hay otra diferencia visible si Iterable.forEachestá iterando sobre una colección sincronizada. En dicha colección, Iterable.forEach toma el bloqueo de la colección una vez y lo retiene en todas las llamadas al método de acción. La Stream.forEachllamada utiliza el spliterator de la colección, que no se bloquea y que se basa en la regla prevaleciente de no interferencia. La colección que respalda la secuencia podría modificarse durante la iteración, y si es así, ConcurrentModificationExceptionpodría producirse un comportamiento inconsistente o no.


Iterable.forEach takes the collection's lock. ¿De dónde es esta información? No puedo encontrar ese comportamiento en las fuentes JDK.
turbanoff


@Stuart, ¿puedes dar más detalles sobre no interferir? Stream.forEach () también lanzará ConcurrentModificationException (al menos para mí).
yuranos

1
@ yuranos87 Muchas colecciones, como las que ArrayListtienen una comprobación bastante estricta de modificaciones concurrentes, y por lo tanto, a menudo se lanzarán ConcurrentModificationException. Pero esto no está garantizado, particularmente para flujos paralelos. En lugar de CME, puede obtener una respuesta inesperada. Considere también modificaciones no estructurales a la fuente de flujo. Para flujos paralelos, no sabe qué hilo procesará un elemento en particular, ni si se ha procesado en el momento en que se modifica. Esto establece una condición de carrera, donde puede obtener resultados diferentes en cada carrera y nunca obtener un CME.
Stuart Marks

30

Esta respuesta se refiere al rendimiento de las diversas implementaciones de los bucles. Es solo marginalmente relevante para los bucles que se llaman MUY A MENUDO (como millones de llamadas). En la mayoría de los casos, el contenido del bucle será, con mucho, el elemento más costoso. Para situaciones en las que realizas bucles con mucha frecuencia, esto podría ser de interés.

Debe repetir estas pruebas en el sistema de destino, ya que esto es específico de la implementación ( código fuente completo ).

Ejecuto openjdk versión 1.8.0_111 en una máquina Linux rápida.

Escribí una prueba que recorre 10 ^ 6 veces sobre una Lista usando este código con diferentes tamaños para integers(10 ^ 0 -> 10 ^ 5 entradas).

Los resultados están a continuación, el método más rápido varía según la cantidad de entradas en la lista.

Pero aún en las peores situaciones, recorrer 10 ^ 5 entradas 10 ^ 6 veces tomó 100 segundos para el peor desempeño, por lo que otras consideraciones son más importantes en prácticamente todas las situaciones.

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

Aquí están mis tiempos: milisegundos / función / número de entradas en la lista. Cada ejecución es de 10 ^ 6 bucles.

                           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

Si repite el experimento, publiqué el código fuente completo . Edite esta respuesta y agregue sus resultados con una notación del sistema probado.


Usando una 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 - 3.4GHz Intel Xeon, 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

Java 11 Hotspot VM - 3.4GHz Intel Xeon, 8 GB, Windows 10 Pro
(misma máquina que la anterior, versión diferente de JDK)

                            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 - 3.4GHz Intel Xeon, 8 GB, Windows 10 Pro
(misma máquina y versión JDK que la anterior, VM diferente)

                            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.8GHz AMD, 64 GB, Windows Server 2016
(misma máquina que la anterior, versión diferente de JDK)

                            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.8GHz AMD, 64 GB, Windows Server 2016
(misma máquina y versión JDK que la anterior, VM diferente)

                            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

La implementación de VM que elija también marca la diferencia Hotspot / OpenJ9 / etc.


3
Esa es una muy buena respuesta, gracias! Pero desde el primer vistazo (y también desde el segundo) no está claro qué método corresponde a qué experimento.
torina

Siento que esta respuesta necesita más votos para la prueba de código :).
Cory

para ejemplos de pruebas +1
Centos

8

No hay diferencia entre los dos que ha mencionado, al menos conceptualmente, Collection.forEach()es solo una abreviatura.

Internamente, la stream()versión tiene algo más de sobrecarga debido a la creación de objetos, pero mirando el tiempo de ejecución tampoco tiene una sobrecarga allí.

Ambas implementaciones terminan iterando sobre los collectioncontenidos una vez, y durante la iteración imprimen el elemento.


La sobrecarga de creación de objetos que mencionas, ¿te refieres a la Streamcreación o los objetos individuales? AFAIK, a Streamno duplica los elementos.
Raffi Khatchadourian

30
Esta respuesta parece contradecir la excelente respuesta escrita por el caballero que desarrolla las bibliotecas centrales de Java en Oracle Corporation.
Dawood ibn Kareem

0

Collection.forEach () usa el iterador de la colección (si se especifica uno). Eso significa que se define el orden de procesamiento de los artículos. Por el contrario, el orden de procesamiento de Collection.stream (). ForEach () no está definido.

En la mayoría de los casos, no importa cuál de los dos elegimos. Las secuencias paralelas nos permiten ejecutar la secuencia en múltiples subprocesos, y en tales situaciones, el orden de ejecución no está definido. Java solo requiere que todos los subprocesos finalicen antes de llamar a cualquier operación de terminal, como Collectors.toList (). Veamos un ejemplo donde primero llamamos a ForEach () directamente en la colección, y segundo, en una secuencia paralela:

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

Si ejecutamos el código varias veces, vemos que list.forEach () procesa los elementos en orden de inserción, mientras que list.parallelStream (). ForEach () produce un resultado diferente en cada ejecución. Una salida posible es:

ABCD CDBA

Otro es:

ABCD DBCA
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.