La diferencia entre Collection.stream (). ForEach () y Collection.forEach ()

1. Introducción

Hay varias opciones para iterar sobre una colección en Java. En este breve tutorial, veremos dos enfoques similares: Collection.stream (). ForEach () y Collection.forEach () .

En la mayoría de los casos, ambos producirán los mismos resultados, sin embargo, veremos algunas diferencias sutiles.

2. Resumen

Primero, creemos una lista para iterar:

List list = Arrays.asList("A", "B", "C", "D");

La forma más sencilla es utilizar el bucle for mejorado:

for(String s : list) { //do something with s } 

Si queremos usar Java de estilo funcional, también podemos usar forEach () . Podemos hacerlo directamente en la colección:

Consumer consumer = s -> { System.out::println }; list.forEach(consumer); 

O podemos llamar a forEach () en el flujo de la colección:

list.stream().forEach(consumer); 

Ambas versiones iterarán sobre la lista e imprimirán todos los elementos:

ABCD ABCD

En este caso simple, no importa qué forEach () usemos.

3. Orden de ejecución

Collection.forEach () usa el iterador de la colección (si se especifica uno). Eso significa que el orden de procesamiento de los artículos está definido. 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 elijamos.

3.1. Secuencias paralelas

Los flujos paralelos nos permiten ejecutar el flujo en varios subprocesos y, en tales situaciones, el orden de ejecución no está definido. Java solo requiere que todos los subprocesos finalicen antes de que se llame 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.

Un posible resultado es:

ABCD CDBA

Otro es:

ABCD DBCA

3.2. Iteradores personalizados

Definamos una lista con un iterador personalizado para iterar sobre la colección en orden inverso:

class ReverseList extends ArrayList { @Override public Iterator iterator() { int startIndex = this.size() - 1; List list = this; Iterator it = new Iterator() { private int currentIndex = startIndex; @Override public boolean hasNext() { return currentIndex >= 0; } @Override public String next() { String next = list.get(currentIndex); currentIndex--; return next; } @Override public void remove() { throw new UnsupportedOperationException(); } }; return it; } } 

Cuando iteramos sobre la lista, nuevamente con forEach () directamente en la colección y luego en la secuencia:

List myList = new ReverseList(); myList.addAll(list); myList.forEach(System.out::print); System.out.print(" "); myList.stream().forEach(System.out::print); 

Obtenemos diferentes resultados:

DCBA ABCD 

La razón de los resultados diferentes es que forEach () usado directamente en la lista usa el iterador personalizado, mientras que stream (). ForEach () simplemente toma los elementos uno por uno de la lista, ignorando el iterador.

4. Modificación de la colección

Muchas colecciones (por ejemplo, ArrayList o HashSet ) no deberían modificarse estructuralmente mientras se iteran sobre ellas. Si un elemento se elimina o se agrega durante una iteración, obtendremos una excepción ConcurrentModification .

Además, las colecciones están diseñadas para fallar rápidamente, lo que significa que la excepción se lanza tan pronto como hay una modificación.

De manera similar, obtendremos una excepción ConcurrentModification cuando agreguemos o eliminemos un elemento durante la ejecución de la canalización de flujo. Sin embargo, la excepción se lanzará más tarde.

Otra sutil diferencia entre los dos métodos forEach () es que Java permite explícitamente modificar elementos usando el iterador. Las corrientes, por el contrario, no deben interferir.

Veamos cómo eliminar y modificar elementos con más detalle.

4.1. Eliminar un elemento

Definamos una operación que elimine el último elemento ("D") de nuestra lista:

Consumer removeElement = s -> { System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } };

Cuando iteramos sobre la lista, el último elemento se elimina después de que se imprime el primer elemento ("A"):

list.forEach(removeElement);

Dado que forEach () es rápido ante fallas, dejamos de iterar y vemos una excepción antes de que se procese el siguiente elemento:

A 4 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList.forEach(ArrayList.java:1252) at ReverseList.main(ReverseList.java:1)

Let's see what happens if we use stream().forEach() instead:

list.stream().forEach(removeElement);

Here, we continue iterating over the whole list before we see an exception:

A 4 B 3 C 3 null 3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at ReverseList.main(ReverseList.java:1)

However, Java does not guarantee that a ConcurrentModificationException is thrown at all. That means we should never write a program that depends on this exception.

4.2. Changing Elements

We can change an element while iterating over a list:

list.forEach(e -> { list.set(3, "E"); });

However, while there is no problem with doing this using either Collection.forEach() or stream().forEach(), Java requires an operation on a stream to be non-interfering. This means that elements shouldn't be modified during the execution of the stream pipeline.

The reason behind this is that the stream should facilitate parallel execution. Here, modifying elements of a stream could lead to unexpected behavior.

5. Conclusion

In this article, we saw some examples that show the subtle differences between Collection.forEach() and Collection.stream().forEach().

However, it's important to note that all the examples shown above are trivial and are merely meant to compare the two ways of iterating over a collection. We shouldn't write code whose correctness relies on the shown behavior.

If we don't require a stream but only want to iterate over a collection, the first choice should be using forEach() directly on the collection.

El código fuente de los ejemplos de este artículo está disponible en GitHub.