Jugué con una solución usando groupingBy , mapping y reducing a la siguiente pregunta: Cree elegantemente un mapa con campos de objetos como clave/valor de flujo de objetos en Java 8 . Resumiendo, el objetivo era conseguir un mapa con la edad como clave y las aficiones de una persona como Set .
Una de las soluciones que se me ocurrió (no es agradable, pero ese no es el punto) tenía un comportamiento extraño.
Con la siguiente lista como entrada:
List<Person> personList = Arrays.asList( new Person(/* name */ "A", /* age */ 23, /* hobbies */ asList("a")), new Person("BC", 24, asList("b", "c")), new Person("D", 23, asList("d")), new Person("E", 23, asList("e")) );y la siguiente solución:
Collector<List<String>, ?, Set<String>> listToSetReducer = Collectors.reducing(new HashSet<>(), HashSet::new, (strings, strings2) -> { strings.addAll(strings2); return strings; }); Map<Integer, Set<String>> map = personList.stream() .collect(Collectors.groupingBy(o -> o.age, Collectors.mapping(o -> o.hobbies, listToSetReducer))); System.out.println("map = " + map);Obtuve:
map = {23=[a, b, c, d, e], 24=[a, b, c, d, e]}claramente no es lo que esperaba. Más bien esperaba esto:
map = {23=[a, d, e], 24=[b, c]} Ahora, si solo reemplazo el orden de (strings, strings2) del operador binario (del colector reductor) a (strings2, strings) obtengo el resultado esperado. Entonces, ¿qué me perdí aquí? ¿Interpreté mal el colector reducing ? ¿O qué parte de la documentación me perdí que hace evidente que mi uso no estaba funcionando como se esperaba?
La versión de Java es 1.8.0_121 si eso importa.
La reducción nunca debe modificar los objetos entrantes. En su caso, está modificando el HashSet entrante que se supone que es el valor de identidad y lo devuelve, por lo que todos los grupos tendrán como resultado la misma instancia de HashSet , que contiene todos los valores.
Lo que necesita es una Reducción mutable , que se puede implementar a través de Collector.of(…) como si ya se hubiera implementado con los recopiladores prediseñados Collectors.toList() , Collectors.toSet() , etc.
Map<Integer, Set<String>> map = personList.stream() .collect(Collectors.groupingBy(o -> o.age, Collector.of(HashSet::new, (s,p) -> s.addAll(p.hobbies), (s1,s2) -> { s1.addAll(s2); return s1; }))); La razón por la que necesitamos un recopilador personalizado es que Java 8 no tiene el recopilador flatMapping , que Java 9 va a presentar. Con eso, la solución se verá así:
Map<Integer, Set<String>> map = personList.stream() .collect(Collectors.groupingBy(o -> o.age, Collectors.flatMapping(p -> p.hobbies.stream(), Collectors.toSet())));