4 Reducción mutable¶
Introducción¶
La operación de recolección es una operación terminal que permite crear una estructura de datos con los resultados del procesamiento de datos asociado a un stream. La operación de recolección también recibe el nombre de operación de reducción mutable. Para llevarla a cabo usaremos el método <R, A> R collect(Collector<? super T, A, R> collector)
El método recibe un objeto de una clase que implemente la interfaz Collector
. Aunque podemos crear nuestras propias clases que implementen dicha interfaz, en la mayoría de las ocasiones podremos utilizar alguno de los recolectores estándar proporcionados por Java a través de la clase auxiliar Collectors
, que contiene métodos estáticos que retornan objetos Collector
correspondientes a los recolectores más habituales. Todos estos métodos están diseñados para funcionar de manera óptima incluso con streams paralelos.
Recolectores a estructuras de datos clásicas¶
La clase Collectors
tiene un conjunto de métodos estáticos que nos permiten recolectar los elementos de un stream y almacenarlos en una estructura de datos:
public static <T> Collector<T, ?, List<T>> toList()
: retorna una lista con los elementos del stream. En Java 16, se ha incorporada también un métodotoList()
a la interfazStream<T>
.public static <T> Collector<T, ?, List<T>> toUnmodifiableList()
: retorna una lista inmutable de los elementos del stream en el orden en que son producidos (encounter order).-
public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory)
: el problema detoList()
ytoSet()
es que no podemos especificar la implementación concreta que queremos que se use. Por ejemplo, no podemos indicar que se use unLinkedList
, en el caso detoList()
, o unTreeSet
, en el caso detoSet()
.Para solucionar este problema, el método recibe un supplier que retorna la estructura de datos concreta en la que queremos que se recolecte el stream.
public class CollectorsClassicDataStructures{ public void show(){ List<Integer> list = List.of(3, 6, 1, 2, 4, 5); List<Integer> listEvenNumbers = list.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); listEvenNumbers.forEach(System.out::println); SortedSet<Integer> tree = list.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toCollection(TreeSet::new)); tree.forEach(System.out::println); } public static void main(String[] args){ new CollectorsClassicDataStructures().show(); } }
-
public static <T, K, V> Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)
: retorna un mapa obtenido de la siguiente manera:- Con el
Function
keyMapper obtiene la clave, de manera que su valor de retorno será usado como clave del elemento en el mapa resultante. Como el mapa resultante no puede tener claves repetidas, si retoma el mismo valor para dos elementos distintos del stream, se lanzará la excepciónIllegalStateException
- Con el
Function
valueMapper se obtiene el valor del elemento en el mapa resultante.
public class CollectorsToMap{ public void show(){ Map<String, Vehicle> map; Vehicle[] vehicles = new Vehicle[6]; vehicles[0] = new Vehicle("9685KMX", 4, "azul"); vehicles[1] = new Vehicle("1235GTR", 2, "rojo"); vehicles[2] = new Vehicle("7314QWE", 4, "verde"); vehicles[3] = new Vehicle("5930POI", 2, "negro"); vehicles[4] = new Vehicle("1705UBG", 4, "blanco"); vehicles[5] = new Vehicle("3495JZA", 2, "naranja"); map = Arrays.stream(vehicles) .collect(Collectors.toMap(Vehicle::getRegistration, vehicle-> vehicle)); map.forEach((k, v) -> System.out.printf("Clave:%s Valor:%s\n", k, v)); } public static void main(String[] args){ new CollectorsToMap().show(); } }
Si nos fijamos en el
Function
valueMapper (vehicle -> vehicle), también podemos usar el método estáticostatic <T> Function<T,T> identity()
, que es una función que siempre devuelve su argumento de entrada:public class FunctionIdentity{ public void show(){ Map<String, Vehicle> map; Vehicle[] vehicles = new Vehicle[6]; vehicles[0] = new Vehicle("9685KMX", 4, "azul"); vehicles[1] = new Vehicle("1235GTR", 2, "rojo"); vehicles[2] = new Vehicle("7314QWE", 4, "verde"); vehicles[3] = new Vehicle("5930POI", 2, "negro"); vehicles[4] = new Vehicle("1705UBG", 4, "blanco"); vehicles[5] = new Vehicle("3495JZA", 2, "naranja"); map = Arrays.stream(vehicles) .collect(Collectors.toMap(Vehicle::getRegistration, Function.identity())); map.forEach((k, v) -> System.out.printf("Clave:%s Valor:%s\n", k, v)); } public static void main(String[] args){ new FunctionIdentity().show(); } }
Tenemos disponible una segunda versión del método para que en lugar de lanzar una excepción, proporcionemos una función de combinación de elementos con la misma clave:
- Con el
-
public static <T, K, U> Collector<T, ?, Map<K, U>> yoMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
: el tercer parámetro indica cómo de deben combinar dos elementos con la misma clave.public class CombinationRepeatedKeys{ public void show(){ Map<String, String> map; Vehicle[] vehicles = new Vehicle[6]; vehicles[0] = new Vehicle("9685KMX", 4, "azul"); vehicles[1] = new Vehicle("1235GTR", 2, "rojo"); vehicles[2] = new Vehicle("7314QWE", 4, "verde"); vehicles[3] = new Vehicle("5930POI", 2, "rojo"); vehicles[4] = new Vehicle("1705UBG", 4, "blanco"); vehicles[5] = new Vehicle("3495JZA", 2, "azul"); map = Arrays.stream(vehicles) .collect(Collectors.toMap( Vehicle::getColour, Vehicle::getRegistration, (r1, r2) -> String.format("Clave:%-7s Valor: %s\n", r1, r2) ) ); map.forEach((k, v) -> System.out.printf("Clave:%-7s Valor:%s\n", k, v)); } public static void main(String[] args){ new CombinationRepeatedKeys().show(); } }
Estas dos versiones del método
toMap
retornan por defecto unHashMap
, por lo que existe una tercera versión para obtener una implementación distinta de la interfazMap
, como por ejemploLinkedHashMap
oTreeMap
: -
public static <T, K, U, M extends Map<K, U>> Collector <T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapFactory)
: recibe unSupplier
como cuarto parámetro para indicar el tipo de mapa que se quiere obtener:public class CombinationRepeatedKeysSupplier{ public void show(){ Map<String, Vehicle> map; Vehicle[] vehicles = new Vehicle[6]; vehicles[0] = new Vehicle("9685KMX", 4, "azul"); vehicles[1] = new Vehicle("1235GTR", 2, "rojo"); vehicles[2] = new Vehicle("7314QWE", 4, "verde"); vehicles[3] = new Vehicle("5930POI", 2, "rojo"); vehicles[4] = new Vehicle("1705UBG", 4, "blanco"); vehicles[5] = new Vehicle("3495JZA", 2, "azul"); map = Arrays.stream(vehicles) .collect(Collectors.toMap(Vehicle::getColour, Vehicle::getRegistration, (r1, r2) -> String.format("%s-%s", r1, r2), TreeMap::new)); map.forEach((k, v) -> System.out.printf("Clave:%-7s Valor:%s\n", k,v)); } public static void main(String[] args){ new CombinationRepeatedKeys().show(); } }
-
Recolección de un stream hacia un array: no se realizará a través de ningún recolector, ni del método
collect
, sino directamente a través del métodotoArray()
de la claseStream
, que retorna unObject[]
, es decir un array de elementos de la claseObjecto
, debido a que los arrays no usan genéricos.public class ToArray{ public void show(){ List<Vehicle> list = new ArrayList<>(); list.add(new Vehicle("9685KMX", 4, "azul")); list.add(new Vehicle("1235GTR", 2, "rojo")); list.add(new Vehicle("7314QWE", 4, "verde")); list.add(new Vehicle("5930POI", 2, "negro")); list.add(new Vehicle("1705UBG", 4, "blanco")); list.add(new Vehicle("3495JZA", 2, "naranja")); Object vehiclesArray[] = list.stream().toArray(); for(Object v: vehiclesArray){ System.out.println(v); } } public static void main(String[] args){ new ToArray().show(); } }
Recolectores de operaciones de reducción básicas¶
La clase Collectors
dispone además de una serie de métodos estáticos que retornan recolectores parecidos a las operaciones de reducción:
-
counting()
: Para obtener el números de elementos. -
Para obtener la sumar de los elementos que son convertidos al tipo indicado mediante la función suministrada:
public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper)
public static <T> Collector<T, ?, Long> summingLong(ToLongFunction<? super T> mapper)
public static <T> Collector<T, ?, Double> summingDouble(ToDoubleFunction<? super T> mapper)
public class CollectorsSumming{ public void show(){ List<Vehicles> list = new ArrayList<>(); list.add(new Vehicle("9685KMX", 4, "azul")); list.add(new Vehicle("1235GTR", 2, "rojo")); list.add(new Vehicle("7314QWE", 4, "verde")); list.add(new Vehicle("5930POI", 2, "negro")); list.add(new Vehicle("1705UBG", 4, "blanco")); list.add(new Vehicle("3495JZA", 2, "naranja")); int sumWheels = list.stream() .collect(Collectors.summingInt(Vehicle::getWheelCount)); System.out.println(sumWheels); // 18 } public static void main(String[] args){ new CollectorsSumming().show(); } }
-
Para obtener el valor mínimo y el máximo atendiendo a un comparador pasado como argumento.
public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator)
public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator)
public class CollectorsMinMax{ public void show(){ Optional<Integer> min, max; List<Integer> list = List.of(30, 23, 24, 57, 8, 15); min = list.stream() .collect(Collectors.minBy(Comparator.naturalOrder())); min.ifPresent(System.out::println); // 8 max = list.stream() .collect(Collectors.maxBy(Comparator.naturalOrder())); max.ifPresent(System.out::println); // 57 } public static void main(String[] args){ new CollectorsMinMax().show(); } }
-
Para obtener la media aritmética de los valores (que son convertidos al tipo indicado mediante la función suministrada). Si el stream no tiene elementos retorna cero:
public static <T> Collector<T, ?, Double> averagingInt(ToIntFunction<? super T> mapper)
public static <T> Collector<T, ?, Double> averagingLong(ToLongFunction<? super T> mapper)
public static <T> Collector<T, ?, Double> averagingDouble(ToDoubleFunction<? super T> mapper)
public class CollectorsAveraging{ public void show(){ double average; List<Integer> list = List.of(30, 23, 24, 57, 8, 15); average = list.stream() .collect(Collectors.averagingInt(Integer::intValue)); System.out.printf("%.2f", average); //26,17 } public static void main(String[] args){ new CollectorsAveraging().show(); } }
-
Para obtener todo lo anterior (número de elementos, suma, mínimo, máximo y la media) de una sola vez:
public static<T> Collector(T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
public static<T> Collector(T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
public static<T> Collector(T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
public class Summarizing{ public void show(){ IntSummaryStatistics oddStatistics = Stream.of(30, 23, 24, 57, 8, 15) .filter(n -> n % 2 != 0) .collect(Collectors.summarizingInt(Integer::intValue)); System.out.println(oddStatistics); } public static void main(String[] args){ new Summarizing().show(); } }
-
Para recolectar con una operación de reducción distinta de las anteriores:
public static <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op)
public class CollectorsReducing { public void show() { Optional<Integer> integerSum = Stream.of(30, 23, 24, 57, 8, 15) .collect(Collectors.reducing((subtotal,element) -> subtotal + element));//Con lambda integerSum.ifPresent(System.out::println);//157 integerSum = Stream.of(30, 23, 24, 57, 8, 15) .collect(Collectors.reducing(Integer::sum));//Con referencia a método integerSum = Stream.<Integer>empty().collect(Collectors.reducing(Integer::sum));//Optional.empty integerSum.ifPresent(System.out::println);//No hace nada } public static void main(String[] args) { new CollectorsReducing().show(); } }
También tenemos otra versión del método que recibe como primer parámetro el valor correspondiente a la identidad: public static <T> Collector<T,?,T> reducing(T identity, BinaryOperator<T> op)
public class CollectorsReducingIdentity {
public void show() {
Integer sum,mult;
sum = Stream.of(30, 23, 24, 57, 8, 15).collect(Collectors.reducing(0, Integer::sum));
System.out.println(sum);//157
sum = Stream.<Integer>empty().collect(Collectors.reducing(0, Integer::sum));
System.out.println(sum);//0
mult = Stream.of(2,3,4).collect(Collectors.reducing(1, Math::multiplyExact));
System.out.println(mult);//24
mult = Stream.<Integer>empty().collect(Collectors.reducing(1,Math::multiplyExact));
System.out.println(mult);//1
}
public static void main(String[] args) {
new CollectorsReducingIdentity().show();
}
}
Existe además una tercera versión del método que recibe como segundo parámetro una función de transformación que será ejecutada sobre cada elemento antes de realizar la recolección: public static <T, U> Collector<T,?,U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
:
public class CollectorsReducingCombiner {
public void show() {
int count = Stream.of("Juan", "Pepe", "Luis", "Ricardo", "Laura").collect(Collectors.reducing(0, element -> {
if(element.length() % 2 == 0) {
return 1;
}
else {
return 0;
}
}, Integer::sum));
System.out.printf("Cuántos nombres con número de caracteres pares: %d", count);//3
}
public static void main(String[] args) {
new CollectorsReducingCombiner().show();
}
}
Como podemos apreciar, todos los recolectores vistos en este apartado son muy similares (casi iguales) a los métodos estándar de reducción que vimos en un apartado anterior. Entonces, ¿por qué existen estos recolectores? El motivo es que, como veremos más adelante, Java nos va a ofrecer la oportunidad de encadenar varios recolectores, de manera que usaremos los recolectores vistos en este apartado normalmente como acompañante de algún otro recolector. De hecho, no se recomienda usar estos recolectores si no es en conjunción con otro recolector. Si se va a emplear de forma aislada, es más óptimo emplear los métodos estándar de recolección que vimos en un apartado anterior.
Recolectores de transformación¶
-
public static Collector<CharSequence,?,String> joining()
: permite obtener unString
correspondiente a la concatenación de los elementos del stream. Solo podremos usar este recolector sobre un stream de elementos de tipoCharSequence
, por lo que es posible que antes hayamos tenido que aplicar una operación de transformación mediante el método map para obtener un stream adecuado.CharSequence
es una interfaz que representa una secuencia de caracteres. Esta interfaz no impone la mutabilidad, por lo tanto, nos podemos encontrar con clases inmutables y mutables que implementen esta interfaz. Por ejemplo,String
es inmutable yStringBuilder
yStringBuffer
son mutablespublic class CollectorsJoining { public void show() { String result = Stream.of("Juan", "Pepe", "Luis","Ricardo", "Laura") .collect(Collectors.joining()); System.out.println(result); } public static void main(String[] args) { new CollectorsJoining().show(); } }
Existe una segunda versión para concatenar los elementos de entrada separados por un delimitador que se pasa por parámetro:
public static Collector<CharSequence,?,String> joining(CharSequence delimiter)
public class CollectorsJoiningDelimiter { public void show() { String result = Stream.of("Juan", "Pepe", "Luis","Ricardo", "Laura") .collect(Collectors.joining(" - ")); System.out.println(result); } public static void main(String[] args) { new CollectorsJoiningDelimiter().show(); } }
Existe una tercera versión del método que permite indicar el prefijo y el sufijo que queremos poner a la cadena resultante de la concatenación:
public static Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
public class CollectorsJoiningPrefixSuffix{ public void show(){ String result = Stream.of("Juan", "Pepe", "Luis", "Ricardo", "Laura") .collect(Collectors.joining(" - ", "Lista de nombres: ", ".")); System.out.println(result); } public static void main(String[] args){ new CollectorsJoiningPrefixSuffix().show(); } }
-
public static <T, U, A, R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)
: realiza alguna operación de transformación sobre los elementos justo antes de aplicarles un recolector que se pasa por parámetro.public class CollectorsMapping{ public void show(){ Vehicle vehicles[] = new Vehicle[6]; vehicles[0] = new Vehicle("9685KMX", 4, "azul"); vehicles[1] = new Vehicle("1235GTR", 2, "rojo"); vehicles[2] = new Vehicle("7314QWE", 4, "verde"); vehicles[3] = new Vehicle("5930POI", 2, "negro"); vehicles[4] = new Vehicle("1705UBG", 4, "blanco"); vehicles[5] = new Vehicle("3495JZA", 2, "naranja"); String result = Arrays.stream(vehicles) .collect(Collectors.mapping(Vehicle::getRegistration, Collectors.joining(", "))); System.out.println(result); } public static void main(String[] args){ new CollectorsMapping().show(); } }
Recolectores de agrupación¶
public static <T, K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
: permite obtener un mapa Map<K, List<T>>
donde las claves son los valores resultantes de aplicar la función de clasificación a los elementos de entrada y los valores son listas que contienen los elementos de entrada que al aplicarles la función de clasificación se obtiene la clave correspondiente, es decir, todos aquellos elementos del stream original que al aplicarles la función clasificadora retornen el mismo valor, dicho valor será la clave y los elementos serán agrupados en la misma lista con dicha clave. Veamos un ejemplo: vamos a crear un mapa donde la clave será el número de ruedas del vehículo. El mapa tendrá dos entradas, una para los vehículos de 2 ruedas y otra para los vehículos de 4 ruedas. Cada clave tendrá una lista con los vehículos que correspondan con dicho número de ruedas:
public class CollectorsGroupingBy{
public void show(){
Map<Integer, List<Vehicle>> map;
Vehicle[] vehicles = new Vehicle[6];
vehicles[0] = new Vehicle("9685KMX", 4, "azul");
vehicles[1] = new Vehicle("1235GTR", 2, "rojo");
vehicles[2] = new Vehicle("7314QWE", 4, "verde");
vehicles[3] = new Vehicle("5930POI", 2, "negro");
vehicles[4] = new Vehicle("1705UBG", 4, "blanco");
vehicles[5] = new Vehicle("3495JZA", 2, "naranja");
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount));
map.forEach((k, v) -> {
System.out.printf("Vehículos con %d ruedas: \n", k);
v.forEach(vehicle -> System.out.printf("%s\n", vehicle));
})
}
public static void main(String[] args){
new CollectorsGroupingBy().show();
}
}
Sin embargo, será muy habitual que queramos realizar algún cálculo sobre la lista de elementos de cada grupo. Para hacernos más sencilla dicha tarea, tenemos disponible otra versión del método: public static <T, K, A, D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
, que recibe un recolector que queremos que se le aplique a la lista de elementos de cada grupo. Para este cometido, podemos usar los recolectores de operaciones de reducción básicas que vimos en el apartado anterior. Si lo aplicamos al ejemplo anterior, podemos obtener cuántos vehículos hay con 2 ruedas y cuántos hay con 4 ruedas:
public class CollectorsGroupingByDownstream{
public void show(){
Map<Integer, Long> map;
Vehicle[] vehicles = new Vehicle[6];
vehicles[0] = new Vehicle("9685KMX", 4, "azul");
vehicles[1] = new Vehicle("1235GTR", 2, "rojo");
vehicles[2] = new Vehicle("7314QWE", 4, "verde");
vehicles[3] = new Vehicle("5930POI", 2, "negro");
vehicles[4] = new Vehicle("1705UBG", 4, "blanco");
vehicles[5] = new Vehicle("3495JZA", 2, "naranja");
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount, Collectors.counting()));
map.forEach((k, v) -> System.out.printf("Número de vehículos con %d ruedas: %d\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingByDownstream().show();
}
}
Estas dos versiones del método groupingBy
retornan por defecto un HashMap
, por lo que existe una tercera versión para obtener una implementación distinta de la interfaz Map
, como por ejemplo LinkedHashMap
o TreeMap
: public static <T, K, D, A, M extends Map<K, D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
:
public class CollectorsGroupingBySupplier{
public void show(){
Map<Integer, Long> map;
Vehicle[] vehicles = new Vehicle[6];
vehicles[0] = new Vehicle("9685KMX", 4, "azul");
vehicles[1] = new Vehicle("1235GTR", 2, "rojo");
vehicles[2] = new Vehicle("7314QWE", 4, "verde");
vehicles[3] = new Vehicle("5930POI", 2, "negro");
vehicles[4] = new Vehicle("1705UBG", 4, "blanco");
vehicles[5] = new Vehicle("3495JZA", 2, "naranja");
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount, LinkedHashMap::new,
Collectors.counting()));
map.forEach((k, v) -> System.out.printf("Número de vehículos con %d ruedas: %d\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingBySupplier().show();
}
}
¿Y si queremos realizar alguna operación de conversión sobre los elementos de la lista de cada grupo antes de aplicarle el recolector downstream? Podemos usar el método Collectors.mapping
que ya conocemos:
public class CollectorsGroupingByMapping{
public void show(){
Map<Integer, String> map;
Vehicle[] vehicles = new Vehicle[6];
vehicles[0] = new Vehicle("9685KMX", 4, "azul");
vehicles[1] = new Vehicle("1235GTR", 2, "rojo");
vehicles[2] = new Vehicle("7314QWE", 4, "verde");
vehicles[3] = new Vehicle("5930POI", 2, "negro");
vehicles[4] = new Vehicle("1705UBG", 4, "blanco");
vehicles[5] = new Vehicle("3495JZA", 2, "naranja");
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount, TreeMap::new,
Collectors.mapping(Vehicle::getRegistration,
Collectors.joining(" - "))));
map.forEach((k, v) -> System.out.printf("Matrículas con %d ruedas: %s\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingByMapping().show();
}
}
Si la función de transformación retorna un stream, podemos usar public static <T, U, A, R> Collector<T,?,R> flatMapping(Function<? super T,? extends Stream<? extends U>> mapper, Collector<? super U,A,R> downstream)
disponible a partir de Java 9.
Por ejemplo, vamos a cambiar la clase Vehicle para registrar modelos de coches con un determinado número de ruedas y una lista de todos los colores en los que está disponible dicho modelo, y si quisiéramos obtener de cada número de ruedas cuántos colores hay disponible podríamos hacer lo siguiente:
public class Vehicle{
private String model;
private int wheelCount;
private List<String> colors;
public Vehicle(String model, int wheelCount, List<String> colors){
this.model = model;
this.wheelCount = wheelCount;
this.colors = colors;
}
public String getModel(){
return model;
}
public int getWheelCount(){
return wheelCount;
}
public List<String> getColors(){
return colors;
}
}
public class CollectorsGroupingByMapping{
public void show(){
Map<Integer, List<Object>> map;
Vehicle[] vehicles = new Vehicle[4];
vehicles[0] = new Vehicle("Audi", 4, List.of("azul", "rojo", "blanco"));
vehicles[1] = new Vehicle("Ford", 4, List.of("naranja", "blanco"));
vehicles[2] = new Vehicle("Audi", 2, List.of("negro", "verde"));
vehicles[3] = new Vehicle("Ford", 2, List.of("negro", "blanco"));
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount, TreeMap::new,
Collectors.mapping(v -> v.getColors(), Collectors.toList())));
map.forEach((k,v) -> System.out.printf("Número de ruedas: %d Colores: %s\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingByMapping().show();
}
}
Si nos fijamos en la salida de consola, salen las sublistas. Para quitarlas, podemos pasar estas listas a streams y luego utilizar el método flatMapping para aplanarlas:
public class CollectorsGroupingByFlatMapping{
public void show(){
Map<Integer, String> map;
Vehicle[] vehicles = new Vehicle[4];
vehicles[0] = new Vehicle("Audi", 4, List.of("azul", "rojo", "blanco"));
vehicles[1] = new Vehicle("Ford", 4, List.of("naranja", "blanco"));
vehicles[2] = new Vehicle("Audi", 2, List.of("negro", "verde"));
vehicles[3] = new Vehicle("Ford", 2, List.of("negro", "blanco"));
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount, TreeMap::new,
Collectors.flatMapping(v -> v.getColors().stream(), Collectors.joining("-"))));
map.forEach((k,v) -> System.out.printf("Número de ruedas: %d Colores: %s\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingByFlatMapping().show();
}
}
Ahora nos encontramos con colores repetidos. Para quitarlos, podemos hacer uso del método public static <T, A, R, RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher)
.Este método recolecta y después se puede realizar un Function sobre el resultado de la recolección.
public class CollectorsGroupingByCollectingAndThen{
public void show(){
Map<Integer, String> map;
Vehicle[] vehicles = new Vehicle[4];
vehicles[0] = new Vehicle("Audi", 4, List.of("azul", "rojo", "blanco"));
vehicles[1] = new Vehicle("Ford", 4, List.of("naranja", "blanco"));
vehicles[2] = new Vehicle("Audi", 2, List.of("negro", "verde"));
vehicles[3] = new Vehicle("Ford", 2, List.of("negro", "blanco"));
map = Arrays.stream(vehicles)
.collect(Collectors.groupingBy(Vehicle::getWheelCount, TreeMap::new,
Collectors.collectingAndThen(Collectors.flatMapping(v ->
v.getColors().stream(), Collectors.toList()),
list -> list.stream().distinct().collect(Collectors.joining("-")))));
map.forEach((k,v) -> System.out.printf("Número de ruedas: %d Colores: %s\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingByCollectingAndThen().show();
}
}
En otras ocasiones, lo que queremos es filtrar los elementos de cada lista en base a algún criterio, antes de aplicarle el recolector downstream. Para ello, Java 9 incorporó el método public static <T, A, R> Collector<T,?,R> filtering(Predicate<? super T> predicate, Collector<? super T,A,R> downstream)
:
public class CollectorsGroupingByFiltering{
public void show(){
Map<String, List<Vehicle>> map;
List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(new Vehicle("Audi", 4, List.of("azul", "rojo")));
vehicles.add(new Vehicle("Ford", 4, List.of("naranja", "blanco")));
vehicles.add(new Vehicle("Audi", 2, List.of("negro", "verde")));
vehicles.add(new Vehicle("Ford", 2, List.of("amarillo", "blanco")));
map = vehicles.stream()
.collect(Collectors.groupingBy(Vehicle::getModel,
Collectors.filtering(v -> v.getWheelCount() == 4, Collectors.toList())));
map.forEach((k,v) -> System.out.printf("Modelo: %s Vehículos de 4 ruedas: %s\n", k, v));
}
public static void main(String[] args){
new CollectorsGroupingByFiltering().show();
}
}
Se pueden crear varios niveles de agrupamiento, aplicando groupingBy como recolector downstream de los elementos de cada grupo:
public class CollectorsVariousLevelsOfGroupingBy{
public void show(){
Map<String, Map<Integer, Long>> map;
List<Vehicle> vehicles = new ArrayList();
vehicles.add(new Vehicle("Audi", 4, List.of("azul", "rojo")));
vehicles.add(new Vehicle("Ford", 4, List.of("naranja", "blanco", "verde")));
vehicles.add(new Vehicle("Seat", 4, List.of("amarillo", "verde")));
vehicles.add(new Vehicle("Audi", 2, List.of("negro")));
vehicles.add(new Vehicle("Ford", 2, List.of("rojo", "blanco")));
vehicles.add(new Vehicle("Seat", 2, List.of("amarillo", "morado")));
map = vehicles.stream()
.collect(Collectors.groupingBy(Vehicle::getModel,
Collectors.groupingBy(Vehicle::getWheelCount,
Collectors.flatMapping(v -> v.getColors().stream(),
Collectors.counting()))));
map.forEach((k,v) -> {
v.forEach((k2, v2) -> {
System.out.printf("Modelo %s con %d ruedas está disponible en %d %s\n", k, k2, v2,
v2 == 1 ? "color" : "colores");
});
});
}
public static void main(String[] args){
new CollectorsVariousLevelsOfGroupingBy().show();
}
}
Modelo Seat con 2 ruedas está disponible en 2 colores
Modelo Seat con 4 ruedas está disponible en 2 colores
Modelo Audi con 2 ruedas está disponible en 1 color
Modelo Audi con 4 ruedas está disponible en 2 colores
Modelo Ford con 2 ruedas está disponible en 2 colores
Modelo Ford con 4 ruedas está disponible en 3 colore
Recolectores de particionado¶
public static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)
: aplica el predicado proporcionado a cada uno de los elementos del stream y crea dos grupos, uno con los que cumplen el predicado y otro con los que no lo cumplen:
public class CollectorsPartitionBy{
public void show(){
Map<Boolean, Long> map;
List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(new Vehicle("Audi", 4, List.of("azul", "rojo")));
vehicles.add(new Vehicle("Ford", 4, List.of("naranja", "blanco", "verde")));
vehicles.add(new Vehicle("Seat", 4, List.of("amarillo", "verde")));
vehicles.add(new Vehicle("Audi", 2, List.of("negro")));
vehicles.add(new Vehicle("Ford", 2, List.of("rojo", "blanco")));
map = vehicles.stream()
.collect(Collectors.partitioningBy(vehicle -> vehicle.getWheelCount() == 4,
Collectors.counting()));
map.forEach((k, v) -> {
System.out.printf("%s hay en %d modelos\n", k ? "Vehículos de 4 ruedas" : "Vehículos que no son de 4 ruedas: ", v);
});
}
public static void main(String[] args){
new CollectorsPartitionBy().show();
}
}
Existe otra versión del método que recibe como segundo argumento un recolector para que sea ejecutado sobre la lista de cada grupo:
public class CollectorsPartitioningByDownstream{
public void show(){
Map<Boolean, List<Vehicle>> map;
List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(new Vehicle("Audi", 4, List.of("azul", "rojo")));
vehicles.add(new Vehicle("Ford", 4, List.of("naranja", "blanco")));
vehicles.add(new Vehicle("Audi", 2, List.of("negro", "verde")));
vehicles.add(new Vehicle("Ford", 2, List.of("amarillo", "blanco")));
map = vehicles.stream()
.collect(Collectors.partitioningBy(v -> v.getWheelCount() == 4));
map.forEach((k, v) -> {
System.out.printf("%s\n", k ? "Vehículos de 4 ruedas" : "Vehículos que no son de 4 ruedas: ");
v.forEach(System.out::println);
});
}
public static void main(String[] args){
new CollectorsPartitioningByDownstream().show();
}
}
Combinación de dos recolectores¶
public static <T, R1, R2, R> Collector<T,?,R> teeing(Collector<? super T,?,R1> downstream1, Collector<? super T,?,R2> downstream2, BiFunction<? super R1,? super R2,R> merger)
: ejecuta ambos recolectores sobre los elementos y después ejecuta sobre los resultados la BiFunction
proporcionada, que combinará ambos resultados, de manera que la combinación será el producto final de la recolección
public class Result{
private Optional<Vehicle> min;
private Optional<Vehicle> max;
public Result(Optional<Vehicle> min, Optional<Vehicle> max){
this.min = min;
this.max = max;
}
public Optional<Vehicle> getMin(){
return min;
}
public Optional<Vehicle> getMax(){
return max;
}
@Override
public String toString(){
return String.format("El vehículo que tiene el mínimo número de colores es %s\nEl vehículo que tiene el máximo número de colores es %s\n", min, max);
}
}
public class CollectorsTeeing{
public void show(){
Result result;
List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(new Vehicle("Audi", 4, List.of("azul", "rojo")));
vehicles.add(new Vehicle("Ford", 4, List.of("naranja", "blanco", "verde")));
vehicles.add(new Vehicle("Seat", 4, List.of("amarillo", "verde")));
vehicles.add(new Vehicle("Audi", 2, List.of("negro")));
vehicles.add(new Vehicle("Ford", 2, List.of("rojo", "blanco")));
vehicles.add(new Vehicle("Seat", 2, List.of("amarillo", "morado")));
result = vehicles.stream()
.collect(Collectors.teeing(Collectors.minBy(Comparator.comparing(v -> v.getColors().size())),
Collectors.maxBy(Comparator.comparing(v -> v.getColors().size())),
Result::new));
System.out.println(result);
}
public static void main(String[] args){
new CollectorsTeeing().show();
}
}
Este método es de la versión 12 de Java. En versiones anteriores, sería necesario operar dos veces sobre el stream, almacenar los resultados intermedios en variables temporales y después combinar las variables temporales.
Streams y checked exceptions¶
Las checked exceptions de Java no congenian demasiado bien con la programación funcional y los stream. En el siguiente ejemplo vemos como nuestro código queda más ofuscado debido a que estamos obligados a capturar la checked exception generada por el constructor de la clase URL
:
List<URL> urls = Stream.of("www.iessaladillo.com/api", "www.iessaladillo.com/css")
.map(s -> s.replace("iessaladillo.com", "iessaladillo.es"))
.map(url -> {
try{
return new URL(url);
} catch(Exception e){
// ...
}
}).collect(toList());
Con el objeto de mejorar la claridad de nuestro código, podemos crear métodos que capturen la checked exception y la relancen como una runtime exception, tal y como hace la siguiente librería https://gist.github.com/jomoespe/ea5c21722b693c09c38bf6286226cd92