package ar.com.javacuriosities.streams; import java.util.ArrayList; import java.util.IntSummaryStatistics; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; /* * Cuando nos referimos a collectors, debemos pensar en una reducción mutable de un Stream, esto quiere * decir, que acumulamos elementos del input en un contenedor mutable (List, Map, String, etc). * Dado que los String no son mutables en realidad estas operaciones generan nuevos Strings en general concatenando los mismos. * * El método collect() es una terminal operation por lo cual termina el Stream, además veremos que tenemos la clase Collectors * la cual provee varios métodos de ayuda * * En base a un Collector podemos crear composiciones, lo cual permite ejecutar un Collector sobre el resultado del anterior */ public class Lesson08Collectors { public static void main(String[] args) { // Podemos hacer un collect para dejar nuestros datos en una Collection Stream<String> streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra"); List<String> listOfName = streamOfNames.collect(Collectors.toList()); System.out.println("List Collector: " + listOfName); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra"); Set<String> setOfName = streamOfNames.collect(Collectors.toSet()); System.out.println("Set Collector: " + setOfName); /* * También podemos pasar nuestro propio Collector, donde tenemos tres parámetros: * - Supplier<R> supplier: Se encarga de proveer la instancia del contenedor que vamos a usar * - BiConsumer<R, ? super T> accumulator: Se encarga de consumir el contenedor y el siguiente elemento * - BiConsumer<R, R> combiner: Se encarga de combinar los resultado de los dos contenedores en caso de un merge */ streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra"); List<String> customCollector = streamOfNames.collect((Supplier<List<String>>) Lesson08Collectors::buildContainer, List::add, Lesson08Collectors::combineContainers); System.out.println("Custom Collector: " + customCollector); //Podemos hacer un collect a una estructura Map, para esto debemos proveer dos funciones una para el key y otra para el value streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra"); // Podemos usar Function.identity() para definir una lambda del estilo object -> object básicamente devuelve lo mismo Map<String, Integer> mapOfNamesAndLengths = streamOfNames.collect(Collectors.toMap(Function.identity(), name-> name.length())); System.out.println("1- Map Collector: " + mapOfNamesAndLengths); // Tenemos algunas variantes para manejar casos donde las key estén duplicadas, en nuestro caso vamos a sumar las longitudes streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Cosme Fulanito"); Map<String, Integer> mapOfDuplicateNamesAndLengths = streamOfNames.collect(Collectors.toMap(Function.identity(), name-> name.length(), (x, y)-> x + y)); System.out.println("2- Map Collector: " + mapOfDuplicateNamesAndLengths); /* * Usando collectors podemos ejecutar operaciones de groupBy, para esto debemos proveer una función * y el resultado es Map<K, List<V>> */ streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Map<Integer, List<String>> groupingByLength = streamOfNames.collect(Collectors.groupingBy(String::length)); System.out.println("Grouping by lenght: " + groupingByLength); // Podría ser el caso donde queremos aplicar un collector al resultado de cada grupo, esto se conoce como downstream collector streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Map<Integer, Long> groupingByLengthAndCounting = streamOfNames.collect(Collectors.groupingBy(String::length, Collectors.counting())); System.out.println("Grouping by lenght and counting: " + groupingByLengthAndCounting); // Otra operación muy común es hacer joining streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); String names = streamOfNames.collect(Collectors.joining()); System.out.println("Join: " + names); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); names = streamOfNames.collect(Collectors.joining(",")); System.out.println("Join with delimiter: " + names); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); // El prefix y suffix son aplicados luego del join names = streamOfNames.collect(Collectors.joining(",", "Guys: ", " See you")); System.out.println("Join with delimiter, prefix and suffix: " + names); // Tenemos varios collectors del tipo numéricos, estos están disponibles tanto para Int, Long y Double streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Double averageLength = streamOfNames.collect(Collectors.averagingInt(String::length)); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Integer sumLength = streamOfNames.collect(Collectors.summingInt(String::length)); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Optional<String> maxByLength = streamOfNames.collect(Collectors.maxBy((x, y) -> x.compareTo(y))); // La operación summarizingInt entrega el resultado de (count, sum, min, max, average) generados sobre la función aplicada streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); IntSummaryStatistics statistics = streamOfNames.collect(Collectors.summarizingInt(String::length)); System.out.println("Average Lenght: " + averageLength); System.out.println("Sum Lenght: " + sumLength); System.out.println("Max Lenght: " + maxByLength.get()); System.out.println("Statistics Lenght: " + statistics); /* * Se dice que varios métodos de Collectors tiene versiones downstream los cuales son usados para recibir un segundo collector que se aplica al resultado * * Collectors.counting(): Esto aplica un count a cada elemento * Collectors.reducing(): Es equivalente a la operación terminal reduce con la diferencia que usamos esto para reducciones de multilevel (O sea downstream) * Collectors.partitioningBy(): Crea un Map<Boolean, List> con el resultado del predicado aplicado a cada elemento * Collectors.mapping(): Nos permite aplicar una función a cada elemento y hacer un downstream de ese mismo resultado */ streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Map<Integer, Long> downStreamCounting = streamOfNames.collect(Collectors.groupingBy(String::length, Collectors.counting())); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Map<Integer, Optional<String>> downStreamReducing = streamOfNames.collect(Collectors.groupingBy(String::length, Collectors.reducing((x, y) -> x + "," + y))); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Map<Integer, Map<Boolean, List<String>>> downStreamPartitioningBy = streamOfNames.collect(Collectors.groupingBy(String::length, Collectors.partitioningBy(x -> ((String) x).length() < 13))); streamOfNames = Stream.of("Cosme Fulanito", "Pablo Marmol", "Pedro Picapiedra", "Super Sonico"); Map<Integer, Set<String>> downStreamMapping = streamOfNames.collect(Collectors.groupingBy(String::length, Collectors.mapping((x) -> x + " (" + x.length() + ")", Collectors.toSet()))); System.out.println("Downstream counting: " + downStreamCounting); System.out.println("Downstream reducing: " + downStreamReducing); System.out.println("Downstream partitioning: " + downStreamPartitioningBy); System.out.println("Downstream mapping: " + downStreamMapping); } private static List<String> buildContainer() { return new ArrayList<>(); } private static void combineContainers(List<String> left, List<String>right) { left.addAll(right); } }