1. Información general
En este tutorial, analizaremos los recopiladores de Java 8, que se utilizan en el paso final del procesamiento de una secuencia .
Si desea leer más sobre Stream API en sí, consulte este artículo.
Si desea ver cómo aprovechar el poder de los recopiladores para el procesamiento en paralelo, consulte este proyecto.
2. El Stream.collect () Método
Stream.collect () es uno de los métodos de terminal de la API Stream de Java 8 . Nos permite realizar operaciones de plegado mutables (reempaquetar elementos en algunas estructuras de datos y aplicar alguna lógica adicional, concatenarlos, etc.) en elementos de datos contenidos en una instancia de Stream .
La estrategia para esta operación se proporciona a través de la implementación de la interfaz del recopilador .
3. Coleccionistas
Todas las implementaciones predefinidas se pueden encontrar en la clase Collectors . Es una práctica común usar la siguiente importación estática con ellos para aprovechar una mayor legibilidad:
import static java.util.stream.Collectors.*;
o simplemente colectores de importación únicos de su elección:
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet;
En los siguientes ejemplos, reutilizaremos la siguiente lista:
List givenList = Arrays.asList("a", "bb", "ccc", "dd");
3.1. Collectors.toList ()
El recopilador ToList se puede utilizar para recopilar todos los elementos de Stream en una instancia de List . Lo importante a recordar es el hecho de que no podemos asumir ninguna implementación de List particular con este método. Si desea tener más control sobre esto, use toCollection en su lugar.
Creemos una instancia de Stream que represente una secuencia de elementos y recopilemos en una instancia de List :
List result = givenList.stream() .collect(toList());
3.1.1. Collectors.toUnmodifiableList ()
Java 10 introdujo una forma conveniente de acumular los elementos de Stream en una lista no modificable :
List result = givenList.stream() .collect(toUnmodifiableList());
Si ahora intentamos modificar la lista de resultados , obtendremos una UnsupportedOperationException :
assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);
3.2. Collectors.toSet ()
El recopilador ToSet se puede utilizar para recopilar todos los elementos de Stream en una instancia de Set . Lo importante a recordar es el hecho de que no podemos asumir ninguna implementación de Set en particular con este método. Si queremos tener más control sobre esto, podemos usar toCollection en su lugar.
Creemos una instancia de Stream que represente una secuencia de elementos y recopilemoslos en una instancia de Set :
Set result = givenList.stream() .collect(toSet());
Un conjunto no contiene elementos duplicados. Si nuestra colección contiene elementos iguales entre sí, aparecen en el Conjunto resultante solo una vez:
List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb"); Set result = listWithDuplicates.stream().collect(toSet()); assertThat(result).hasSize(4);
3.2.1. Collectors.toUnmodifiableSet ()
Desde Java 10 podemos crear fácilmente un conjunto no modificable usando el colector toUnmodifiableSet () :
Set result = givenList.stream() .collect(toUnmodifiableSet());
Cualquier intento de modificar el conjunto de resultados terminará con UnsupportedOperationException :
assertThatThrownBy(() -> result.add("foo")) .isInstanceOf(UnsupportedOperationException.class);
3.3. Collectors.toCollection ()
Como probablemente ya haya notado, cuando usa los colectores toSet y toList , no puede hacer suposiciones sobre sus implementaciones. Si desea utilizar una implementación personalizada, deberá utilizar el recopilador toCollection con una colección proporcionada de su elección.
Creemos una instancia de Stream que represente una secuencia de elementos y recopilemos en una instancia de LinkedList :
List result = givenList.stream() .collect(toCollection(LinkedList::new))
Tenga en cuenta que esto no funcionará con ninguna colección inmutable. En tal caso, necesitaría escribir una implementación de recopilador personalizada o utilizar collectAndThen .
3.4. Coleccionistas . para asignar()
El recopilador ToMap se puede utilizar para recopilar elementos de Stream en una instancia de Map . Para hacer esto, necesitamos proporcionar dos funciones:
- keyMapper
- valueMapper
keyMapper se utilizará para extraer una clave Map de un elemento Stream , y valueMapper se utilizará para extraer un valor asociado con una clave determinada.
Recopilemos esos elementos en un mapa que almacena cadenas como claves y sus longitudes como valores:
Map result = givenList.stream() .collect(toMap(Function.identity(), String::length))
Function.identity () es solo un atajo para definir una función que acepta y devuelve el mismo valor.
¿Qué sucede si nuestra colección contiene elementos duplicados? Al contrario de toSet , toMap no filtra silenciosamente los duplicados. Es comprensible: ¿cómo debería saber qué valor elegir para esta clave?
List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb"); assertThatThrownBy(() -> { listWithDuplicates.stream().collect(toMap(Function.identity(), String::length)); }).isInstanceOf(IllegalStateException.class);
Tenga en cuenta que toMap ni siquiera evalúa si los valores también son iguales. Si ve claves duplicadas, inmediatamente lanza una IllegalStateException .
En tales casos con colisión de claves, deberíamos usar toMap con otra firma:
Map result = givenList.stream() .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));
El tercer argumento aquí es un BinaryOperator , donde podemos especificar cómo queremos que se manejen las colisiones. En este caso, elegiremos cualquiera de estos dos valores en colisión porque sabemos que las mismas cadenas siempre tendrán las mismas longitudes también.
3.4.1. Collectors.toUnmodifiableMap ()
De manera similar a List sy Set s, Java 10 introdujo una forma fácil de recopilar elementos Stream en un mapa no modificable :
Map result = givenList.stream() .collect(toMap(Function.identity(), String::length))
Como podemos ver, si intentamos poner una nueva entrada en un mapa de resultados , obtendremos UnsupportedOperationException :
assertThatThrownBy(() -> result.put("foo", 3)) .isInstanceOf(UnsupportedOperationException.class);
3.5. Colectores .c ollectingAndThen ()
CollectingAndThen is a special collector that allows performing another action on a result straight after collecting ends.
Let's collect Stream elements to a List instance and then convert the result into an ImmutableList instance:
List result = givenList.stream() .collect(collectingAndThen(toList(), ImmutableList::copyOf))
3.6. Collectors.joining()
Joining collector can be used for joining Stream elements.
We can join them together by doing:
String result = givenList.stream() .collect(joining());
which will result in:
"abbcccdd"
You can also specify custom separators, prefixes, postfixes:
String result = givenList.stream() .collect(joining(" "));
which will result in:
"a bb ccc dd"
or you can write:
String result = givenList.stream() .collect(joining(" ", "PRE-", "-POST"));
which will result in:
"PRE-a bb ccc dd-POST"
3.7. Collectors.counting()
Counting is a simple collector that allows simply counting of all Stream elements.
Now we can write:
Long result = givenList.stream() .collect(counting());
3.8. Collectors.summarizingDouble/Long/Int()
SummarizingDouble/Long/Int is a collector that returns a special class containing statistical information about numerical data in a Stream of extracted elements.
We can obtain information about string lengths by doing:
DoubleSummaryStatistics result = givenList.stream() .collect(summarizingDouble(String::length));
In this case, the following will be true:
assertThat(result.getAverage()).isEqualTo(2); assertThat(result.getCount()).isEqualTo(4); assertThat(result.getMax()).isEqualTo(3); assertThat(result.getMin()).isEqualTo(1); assertThat(result.getSum()).isEqualTo(8);
3.9. Collectors.averagingDouble/Long/Int()
AveragingDouble/Long/Int is a collector that simply returns an average of extracted elements.
We can get average string length by doing:
Double result = givenList.stream() .collect(averagingDouble(String::length));
3.10. Collectors.summingDouble/Long/Int()
SummingDouble/Long/Int is a collector that simply returns a sum of extracted elements.
We can get a sum of all string lengths by doing:
Double result = givenList.stream() .collect(summingDouble(String::length));
3.11. Collectors.maxBy()/minBy()
MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.
We can pick the biggest element by doing:
Optional result = givenList.stream() .collect(maxBy(Comparator.naturalOrder()));
Notice that returned value is wrapped in an Optional instance. This forces users to rethink the empty collection corner case.
3.12. Collectors.groupingBy()
GroupingBy collector is used for grouping objects by some property and storing results in a Map instance.
We can group them by string length and store grouping results in Set instances:
Map
result = givenList.stream() .collect(groupingBy(String::length, toSet()));
This will result in the following being true:
assertThat(result) .containsEntry(1, newHashSet("a")) .containsEntry(2, newHashSet("bb", "dd")) .containsEntry(3, newHashSet("ccc"));
Notice that the second argument of the groupingBy method is a Collector and you are free to use any Collector of your choice.
3.13. Collectors.partitioningBy()
PartitioningBy is a specialized case of groupingBy that accepts a Predicate instance and collects Stream elements into a Map instance that stores Boolean values as keys and collections as values. Under the “true” key, you can find a collection of elements matching the given Predicate, and under the “false” key, you can find a collection of elements not matching the given Predicate.
You can write:
Map
result = givenList.stream() .collect(partitioningBy(s -> s.length() > 2))
Which results in a Map containing:
{false=["a", "bb", "dd"], true=["ccc"]}
3.14. Collectors.teeing()
Let's find the maximum and minimum numbers from a given Stream using the collectors we've learned so far:
List numbers = Arrays.asList(42, 4, 2, 24); Optional min = numbers.stream().collect(minBy(Integer::compareTo)); Optional max = numbers.stream().collect(maxBy(Integer::compareTo)); // do something useful with min and max
Here, we're using two different collectors and then combining the result of those two to create something meaningful. Before Java 12, in order to cover such use cases, we had to operate on the given Stream twice, store the intermediate results into temporary variables and then combine those results afterward.
Fortunately, Java 12 offers a built-in collector that takes care of these steps on our behalf: all we have to do is provide the two collectors and the combiner function.
Since this new collector tees the given stream towards two different directions, it's called teeing:
numbers.stream().collect(teeing( minBy(Integer::compareTo), // The first collector maxBy(Integer::compareTo), // The second collector (min, max) -> // Receives the result from those collectors and combines them ));
This example is available on GitHub in the core-java-12 project.
4. Custom Collectors
If you want to write your Collector implementation, you need to implement Collector interface and specify its three generic parameters:
public interface Collector {...}
- T – the type of objects that will be available for collection,
- A – the type of a mutable accumulator object,
- R – the type of a final result.
Let's write an example Collector for collecting elements into an ImmutableSet instance. We start by specifying the right types:
private class ImmutableSetCollector implements Collector
{...}
Since we need a mutable collection for internal collection operation handling, we can't use ImmutableSet for this; we need to use some other mutable collection or any other class that could temporarily accumulate objects for us.
In this case, we will go on with an ImmutableSet.Builder and now we need to implement 5 methods:
- Supplier
supplier() - BiConsumer
accumulator() - BinaryOperator
combiner() - Function
finisher() - Set characteristics()
The supplier()method returns a Supplier instance that generates an empty accumulator instance, so, in this case, we can simply write:
@Override public Supplier
supplier() { return ImmutableSet::builder; }
The accumulator() method returns a function that is used for adding a new element to an existing accumulator object, so let's just use the Builder‘s add method.
@Override public BiConsumer
accumulator() { return ImmutableSet.Builder::add; }
The combiner()method returns a function that is used for merging two accumulators together:
@Override public BinaryOperator
combiner() { return (left, right) -> left.addAll(right.build()); }
The finisher() method returns a function that is used for converting an accumulator to final result type, so in this case, we will just use Builder‘s build method:
@Override public Function
finisher() { return ImmutableSet.Builder::build; }
El método features () se utiliza para proporcionarle a Stream información adicional que se utilizará para optimizaciones internas. En este caso, no prestamos atención al orden de los elementos en un Conjunto por lo que usaremos Características . Para obtener más información sobre este tema, consulte Características 'JavaDoc.
@Override public Set characteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); }
Aquí está la implementación completa junto con el uso:
public class ImmutableSetCollector implements Collector
{ @Override public Supplier
supplier() { return ImmutableSet::builder; } @Override public BiConsumer
accumulator() { return ImmutableSet.Builder::add; } @Override public BinaryOperator
combiner() { return (left, right) -> left.addAll(right.build()); } @Override public Function
finisher() { return ImmutableSet.Builder::build; } @Override public Set characteristics() { return Sets.immutableEnumSet(Characteristics.UNORDERED); } public static ImmutableSetCollector toImmutableSet() { return new ImmutableSetCollector(); }
y aquí en acción:
List givenList = Arrays.asList("a", "bb", "ccc", "dddd"); ImmutableSet result = givenList.stream() .collect(toImmutableSet());
5. Conclusión
En este artículo, exploramos en profundidad los recopiladores de Java 8 y mostramos cómo implementar uno. Asegúrese de verificar uno de mis proyectos que mejora las capacidades del procesamiento paralelo en Java.
Todos los ejemplos de código están disponibles en GitHub. Puede leer más artículos interesantes en mi sitio.