Guía de Java Streams: forEach() con ejemplos

En este tutorial, repasaremos el método Java Streams forEach(). Cubriremos el uso básico y ejemplos de forEach() en una lista, un mapa y un conjunto.

Introducción

El método forEach() es parte de la interfaz Stream y se usa para ejecutar una operación específica, definida por un Consumer.

La interfaz Consumer representa cualquier operación que toma un argumento como entrada y no tiene salida. Este tipo de comportamiento es aceptable porque el método forEach() se usa para cambiar el estado del programa mediante efectos secundarios, no tipos de devolución explícitos.

Por lo tanto, los mejores candidatos objetivo para Consumidores son las funciones lambda y las referencias de métodos. Vale la pena señalar que forEach() se puede usar en cualquier Colección.

forEach() en la lista

El método forEach() es una operación de terminal, lo que significa que después de llamar a este método, se materializará el flujo junto con todas sus transformaciones integradas. Es decir, "ganarán sustancia", en lugar de ser transmitidos.

Vamos a generar una pequeña lista:

1
2
3
4
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);

Tradicionalmente, podría escribir un bucle para-cada para pasar por él:

1
2
3
for (Integer element : list) {
    System.out.print(element + " ");
}

Esto imprimiría:

1
1 2 3

Alternativamente, podemos usar el método forEach() en un Stream:

1
2
3
list.stream().forEach((k) -> {
    System.out.print(k + " ");
});

Esto también genera:

1
1 2 3

We can make this even simpler via a referencia del método:

1
list.stream().forEach(System.out::println);

forEach() en el mapa

El método forEach() es realmente útil si queremos evitar encadenar muchos métodos de transmisión. Generemos un mapa con algunas películas y sus respectivos puntajes en IMDB:

1
2
3
4
5
6
Map<String, Double> map = new HashMap<String, Double>();
map.put("Forrest Gump", 8.8);
map.put("The Matrix", 8.7);
map.put("The Hunt", 8.3);
map.put("Monty Python's Life of Brian", 8.1);
map.put("Who's Singin' Over There?", 8.9);

Ahora, imprimamos los valores de cada película que tenga una puntuación superior a 8.4:

1
2
3
4
map.entrySet()
        .stream()
        .filter(entry -> entry.getValue() > 8.4)
        .forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));

Esto resulta en:

1
2
3
Forrest Gump: 8.8
The Matrix: 8.7
Who's Singin' Over There?: 8.9

Aquí, convertimos un ‘Mapa’ en un ‘Conjunto’ a través de ’entrySet()’, lo transmitimos, lo filtramos en función de la puntuación y finalmente los imprimimos a través de ‘forEach()’. En lugar de basar esto en el retorno de filter(), podríamos haber basado nuestra lógica en los efectos secundarios y omitir el método filter():

1
2
3
4
5
6
7
8
map.entrySet()
        .stream()
        .forEach(entry -> {
            if (entry.getValue() > 8.4) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
                }
            }
        );

Esto resulta en:

1
2
3
Forrest Gump: 8.8
The Matrix: 8.7
Who's Singin' Over There?: 8.9

Finalmente, podemos omitir los métodos stream() y filter() comenzando con forEach() al principio:

1
2
3
4
5
map.forEach((k, v) -> {
        if (v > 8.4) {
            System.out.println(k + ": " + v);
         }
     });

Esto resulta en:

1
2
3
Forrest Gump: 8.8
The Matrix: 8.7
Who's Singin' Over There?: 8.9

forEach() en el conjunto

Echemos un vistazo a cómo podemos usar el método forEach en un Set en un contexto un poco más tangible. Primero, definamos una clase que represente a un Empleado de una empresa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Employee {
    private String name;
    private double workedHours;
    private double dedicationScore;
    
    // Constructor, Getters and Setters
    
    public void calculateDedication() {
        dedicationScore = workedHours*1.5;
    }
    
    public void receiveReward() {
        System.out.println(String
            .format("%s just got a reward for being a dedicated worker!", name));
    }
}

Imaginando que somos el gerente, querremos elegir a ciertos empleados que han trabajado horas extras y premiarlos por el trabajo duro. Primero, hagamos un Set:

1
2
3
4
5
6
 Set<Employee> employees = new HashSet<Employee>();

 employees.add(new Employee("Vladimir", 60));
 employees.add(new Employee("John", 25));
 employees.add(new Employee("David", 40));
 employees.add(new Employee("Darinka", 60));

Luego, calculemos el puntaje de dedicación de cada empleado:

1
employees.stream().forEach(Employee::calculateDedication);

Ahora que cada empleado tiene una puntuación de dedicación, eliminemos a los que tienen una puntuación demasiado baja:

1
2
3
4
Set<Employee> regular = employees.stream()
        .filter(employee -> employee.getDedicationScore() <= 60)
        .collect(Collectors.toSet());
employees.removeAll(regular);

Finalmente, recompensemos a los empleados por su arduo trabajo:

1
employees.stream().forEach(employee -> employee.receiveReward());

Y para mayor claridad, imprimamos los nombres de los afortunados trabajadores:

1
2
System.out.println("Awarded employees:");
employees.stream().map(employee -> employee.getName()).forEach(employee -> System.out.println(employee));

Después de ejecutar el código anterior, obtenemos el siguiente resultado:

1
2
3
4
5
Vladimir just got a reward for being a dedicated worker!
Darinka just got a reward for being a dedicated worker!
Awarded employees:
Vladimir
Darinka

Efectos secundarios frente a valores de retorno

El objetivo de cada comando es evaluar la expresión de principio a fin. Todo lo que hay en el medio es un efecto secundario. En este contexto, significa alterar el estado, el flujo o las variables sin devolver ningún valor.

Echemos un vistazo a la diferencia en otra lista:

1
List<Integer> targetList = Arrays.asList(1, -2, 3, -4, 5, 6, -7);

Este enfoque se basa en los valores devueltos de un ArrayList:

1
2
3
4
5
long result1 = targetList
        .stream()
        .filter(integer -> integer > 0)
        .count();
System.out.println("Result: " + result1);

Esto resulta en:

1
Result: 4

Y ahora, en lugar de basar la lógica del programa en el tipo de retorno, realizaremos un forEach() en la secuencia y agregaremos los resultados a un AtomicInteger (las secuencias funcionan simultáneamente):

1
2
3
4
5
6
7
AtomicInteger result2 = new AtomicInteger();

targetList.stream().forEach(integer -> {
            if (integer > 0)
                result2.addAndGet(1);
            });
System.out.println("Result: " + result2);

Conclusión

El método forEach() es un método realmente útil para iterar sobre colecciones en Java con un enfoque funcional.

En ciertos casos, pueden simplificar enormemente el código y mejorar la claridad y la brevedad. En este artículo, repasamos los conceptos básicos del uso de forEach() y luego cubrimos ejemplos del método en List, Map y Set.

Hemos cubierto la diferencia entre el bucle for-each y el forEach(), así como la diferencia entre basar la lógica en los valores de retorno versus los efectos secundarios.

Licensed under CC BY-NC-SA 4.0