2017-03-23 29 views
5

I bawił się z roztworu za pomocą groupingBy, mapping i reducing na następujące pytanie: Elegantly create map with object fields as key/value from object stream in Java 8. Podsumowując, celem było uzyskanie mapy z wiekiem jako kluczem i hobby osoby jako Set.Java 8 stream.collect (... groupingBy (... mapping (... zmniejszenie))) zmniejszenie BinaryOperator-usage

Jedno z rozwiązań, które wymyśliłem (niezbyt fajne, ale nie o to chodzi) miało dziwne zachowanie.

Z poniższej listy jako wejście:

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")) 
); 

i następujące rozwiązanie:

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); 

mam:

map = {23=[a, b, c, d, e], 24=[a, b, c, d, e]} 

wyraźnie nie to, czego się spodziewałem. I raczej spodziewać to:

map = {23=[a, d, e], 24=[b, c]} 

Teraz, jeśli po prostu zastąpić kolejność (strings, strings2) operatorem binarnym (kolektora redukującego) (strings2, strings) mogę uzyskać oczekiwany rezultat. Więc, za czym tęskniłem? Czy źle interpretowałem reducing -lekser? A może brakowało mi dokumentacji, która sprawiała, że ​​moje użycie nie działało zgodnie z oczekiwaniami?

Wersja Java to 1.8.0_121, jeśli to ma znaczenie.

Odpowiedz

9

Redukcja nigdy nie powinna modyfikować przychodzących obiektów. W twoim przypadku modyfikujesz przychodzącą wartość HashSet, która ma być wartością tożsamości i zwraca ją, aby wszystkie grupy miały tę samą instancję HashSet, zawierającą wszystkie wartości.

Co trzeba to Mutable Reduction, które mogą być realizowane poprzez Collector.of(…) jak to zostało już wdrożone z kolektorów gotowych Collectors.toList(), Collectors.toSet() itp

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; 
     }))); 

Powodem, musimy niestandardową kolektor w ogóle , czy Java 8 nie ma kolektora flatMapping, który wprowadzi Java 9. Z tym, rozwiązanie będzie wyglądać następująco:

Map<Integer, Set<String>> map = personList.stream() 
    .collect(Collectors.groupingBy(o -> o.age, 
     Collectors.flatMapping(p -> p.hobbies.stream(), Collectors.toSet()))); 
+0

Ciekawe .. Początkowo próbowałem nawet do stwierdzenia, że ​​'flatMapping' w' Collectors' (lub zastosować 'hobbies.stream()' i pracować z nim) . Dobrze wiedzieć, że to nadchodzi. Och, drogi, pomieszałem kombinator z operatorem. To był tylko zbieg okoliczności, że zmiana parametrów przyniosła właściwy rezultat. Dzięki za wyjaśnienie! – Roland

+2

Tak, w kontekście sekwencyjnym funkcja redukcji będzie zawsze oceniana jako 'f (poprzednia, następna)' podczas gdy 'previous' będzie wartością tożsamości dla pierwszej oceny i poprzednim wynikiem dla kolejnych ocen. Tak więc '(a, b) -> a' zawsze kończy się wartością tożsamości, podczas gdy' (a, b) -> b' używa świeżego zestawu stworzonego przez funkcję mappera. Ale w równoległej ocenie oba argumenty mogą być wynikiem wcześniejszej częściowej oceny, a ponieważ częściowe wyniki mogą być puste, albo argument może być wartością tożsamości, więc użycie drugiego argumentu nie jest niezawodnym rozwiązaniem. – Holger