Java Collect(): cómo utilizar operaciones de reducción

Los Stream Collectors son una potente función de la Java 8 Stream API que permiten recoger y procesar datos de forma eficiente. Explicamos la estructura y los posibles usos del método Java Collect.

¿Cuáles son los ámbitos de aplicación de Java Collect()?

Un Stream Collector puede utilizarse para crear una lista, un conjunto o un mapa a partir de un flujo (stream). Un stream es una secuencia de elementos que se procesan uno tras otro. La interfaz del Collector proporciona una serie de operaciones de reducción para los datos en un flujo de canalización. Estas son operaciones finales que recopilan y fusionan los resultados de los pasos intermedios.

Los collectors pueden ser utilizados, por ejemplo, para filtrar o clasificar objetos de un stream. También es posible realizar agregaciones, como la suma de números, la concatenación de cadenas o el conteo de elementos. Además, los collectors disponen de funciones para transformar el contenido de un stream en una estructura determinada. De esta forma, por ejemplo, se puede convertir una lista en un mapa. Las agrupaciones ayudan a categorizar los elementos con determinadas propiedades o condiciones. Sin embargo, la mayor ventaja de los stream collectors es que permiten procesar datos de manera paralela utilizando múltiples threads (hilos). Esto permite que las operaciones, sobre todo con grandes cantidades de datos, se realicen de manera más rápida y eficiente.

La sintaxis de Java Collect()

Este método recibe como argumento un collector que indica cómo recolectar y agregar los elementos del stream. Un collector es una interfaz que ofrece diferentes métodos para agrupar los elementos del stream de una forma específica, como una lista, un conjunto o un mapa.

Existen dos métodos en Java Collect:

  1. <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)
  2. <R, A> R collect(Collector<? super T, A, R> collector)

La primera variante tiene tres funciones como argumentos:

  • supplier: crea un container que se utiliza para los resultados intermedios.
  • accumulator: calcula el resultado final.
  • combiner: combina los resultados de operaciones de flujo paralelas.

Estos collectors predefinidos ya están incluidos en la biblioteca estándar y pueden importarse y utilizarse fácilmente.

La segunda variante toma un collector como argumento y devuelve el resultado.

  • R: el tipo del resultado
  • T: el tipo de los elementos del stream
  • A: el tipo del accumulator que almacena el estado intermedio de la operación del collector
  • collector: realiza la operación de reducción.

Mediante esta variante, los desarrolladores tienen la posibilidad de crear colectores personalizados que se ajustan específicamente a sus necesidades, dando así una mayor flexibilidad y control al proceso de reducción.

Ejemplos prácticos del uso de Java Collect()

A continuación, presentamos varias funciones del método Stream.collect() para ilustrar su uso. Es recomendable que estés familiarizado con los operadores Java antes de adentrarte en el collection framework.

Concatenar una lista de cadenas

Con Java Collect() puedes concatenar una lista de cadenas para obtener una nueva cadena:

List<String> letters = List.of("a", "b", "c", "d", "e");
// without combiner function
StringBuilder result = letters.stream().collect(StringBuilder::new, (x, y) -> x.append(y),
    (a, b) -> a.append(",").append(b));
System.out.println(result.toString());
// with combiner function
StringBuilder result1 = letters.parallelStream().collect(StringBuilder::new, (x, y) -> x.append(y),
    (a, b) -> a.append(",").append(b));
System.out.println(result1.toString());
Java

Obtenemos este resultado:

abcde
a, b, c, d, e
Java

En el primer cálculo, solo hay una instancia StringBuilder y no se ha utilizado ninguna función combinadora. Por lo tanto, el resultado es abcde.

En el segundo resultado, vemos que la función combinadora ha fusionado las instancias StringBuilder y las ha separado con una coma.

Recoger los elementos de una lista con toList()

Podemos seleccionar ciertos elementos de una lista con la función filter() y almacenarlos en una nueva lista con toList().

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddNumbers = numbers.stream().filter(x -> x % 2 != 0).collect(Collectors.toList());
System.out.println(oddNumbers);
Java

La nueva lista ahora solo contiene números impares:

[1, 3, 5, 7]
Java

Recoger los elementos de un conjunto con toSet()

De la misma forma, podemos crear un nuevo conjunto (set) a partir de los elementos seleccionados. El orden en un conjunto puede ser no ordenado.

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
Set<Integer> evenNumbers = numbers.parallelStream().filter(x -> x % 2 == 0).collect(Collectors.toSet());
System.out.println(evenNumbers);
Java

Esto nos da este resultado:

[2, 4, 6]
Java

Recopilar elementos en un mapa con toMap()

Un mapa en conjunto con Collect() de Java asigna a cada clave un valor.

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
Map<Integer, String> mapEvenNumbers = numbers.parallelStream().filter(x -> x % 2 == 0)
    .collect(Collectors.toMap(Function.identity(), x -> String.valueOf(x)));
System.out.println(mapEvenNumbers);
Java

En el resultado vemos que la entrada formada por números pares ha sido asignada a sus valores idénticos:

{2=2, 4=4, 6=6}
Java

Combinar elementos de una cadena con joining()

El método joining() añade cada elemento del flujo en el orden en que aparecen y utiliza un separador para separar los elementos. El separador se pasa como argumento a joining(). Si no se especifica ningún separador, joining() utiliza la cadena vacía "".

jshell> String result1 = Stream.of("a", "b", "c").collect(Collectors.joining());
jshell> String result2 = Stream.of("a", "b", "c").collect(Collectors.joining(",", "{", "}"));
Java

Los resultados son los siguientes:

result1 ==> "abc"
result2 ==> "{a,b,c}"
Java