2015-08-18 25 views
8

Chcę użyć strumienia Java 8 i grupować według jednego klasyfikatora, ale mieć wiele funkcji kolektora. Zatem podczas grupowania obliczana jest na przykład średnia i suma jednego pola (lub może innego pola).Java 8 Strumień: grupowanie z wieloma kolektorami

Staram się uprościć to trochę na przykładzie:

public void test() { 
    List<Person> persons = new ArrayList<>(); 
    persons.add(new Person("Person One", 1, 18)); 
    persons.add(new Person("Person Two", 1, 20)); 
    persons.add(new Person("Person Three", 1, 30)); 
    persons.add(new Person("Person Four", 2, 30)); 
    persons.add(new Person("Person Five", 2, 29)); 
    persons.add(new Person("Person Six", 3, 18)); 

    Map<Integer, Data> result = persons.stream().collect(
      groupingBy(person -> person.group, multiCollector) 
    ); 
} 

class Person { 
    String name; 
    int group; 
    int age; 

    // Contructor, getter and setter 
} 

class Data { 
    long average; 
    long sum; 

    public Data(long average, long sum) { 
     this.average = average; 
     this.sum = sum; 
    } 

    // Getter and setter 
} 

Wynik powinien być mapa, która kojarzy wynik grupowania jak

1 => Data(average(18, 20, 30), sum(18, 20, 30)) 
2 => Data(average(30, 29), sum(30, 29)) 
3 => .... 

To działa perfekcyjnie z jednej funkcji, takich jak "Collectors.counting()", ale lubię łączyć więcej niż jeden (najlepiej nieskończenie z Listą).

List<Collector<Person, ?, ?>> 

Czy można zrobić coś takiego?

+0

Czy dobrze rozumiem, że 'Data' klasa jest tylko zastępczy dla kolekcji flatmap-funkcji? Każda z tych funkcji musi wykonać tę samą operację na wszystkich grupach (np. Najpierw obliczyć grupy na wiek, po drugie całkowity wiek itp.)? –

+0

Kiedy cię poprawiłem, tak myślę. Po prostu użyłem go do przechowywania wielu danych w jednym obiekcie, a jednocześnie mogłem zidentyfikować który jest który. Może to być tablica lub mapa z kluczem = nazwa funkcji, wartość = funkcjaRozmiar. – PhilippS

Odpowiedz

12

dla betonu problemu sumowanie i uśrednianie, użyj collectingAndThen wraz z summarizingDouble:

Map<Integer, Data> result = persons.stream().collect(
     groupingBy(Person::getGroup, 
       collectingAndThen(summarizingDouble(Person::getAge), 
         dss -> new Data((long)dss.getAverage(), (long)dss.getSum())))); 

Dla bardziej ogólny problem (zbieraj różne rzeczy o twoich Osobach), Można utworzyć złożoną kolektor tak:

// Individual collectors are defined here 
List<Collector<Person, ?, ?>> collectors = Arrays.asList(
     Collectors.averagingInt(Person::getAge), 
     Collectors.summingInt(Person::getAge)); 

@SuppressWarnings("unchecked") 
Collector<Person, List<Object>, List<Object>> complexCollector = Collector.of(
    () -> collectors.stream().map(Collector::supplier) 
     .map(Supplier::get).collect(toList()), 
    (list, e) -> IntStream.range(0, collectors.size()).forEach(
     i -> ((BiConsumer<Object, Person>) collectors.get(i).accumulator()).accept(list.get(i), e)), 
    (l1, l2) -> { 
     IntStream.range(0, collectors.size()).forEach(
      i -> l1.set(i, ((BinaryOperator<Object>) collectors.get(i).combiner()).apply(l1.get(i), l2.get(i)))); 
     return l1; 
    }, 
    list -> { 
     IntStream.range(0, collectors.size()).forEach(
      i -> list.set(i, ((Function<Object, Object>)collectors.get(i).finisher()).apply(list.get(i)))); 
     return list; 
    }); 

Map<Integer, List<Object>> result = persons.stream().collect(
     groupingBy(Person::getGroup, complexCollector)); 

Mapa wartości są listy, gdzie pierwszym elementem jest wynikiem stosowania pierwszego kolektora i tak dalej. Możesz dodać niestandardowy krok finisher za pomocą Collectors.collectingAndThen(complexCollector, list -> ...), aby przekonwertować tę listę na coś bardziej odpowiedniego.

+0

OK, które jest interesujące i myślę, że odnosi się do odpowiedzi Petera Lawrey'a. Ale to oznaczałoby, że nie jest elastyczny dla każdego rodzaju funkcji, jaką myślę. Chcę użyć listy funkcji (kolektory). Z twoim rozwiązaniem myślę, że ograniczam się do tego, co podsumowujeDouble. – PhilippS

+0

@PhilippS, zredagowano odpowiedź. Proszę spojrzeć. –

+0

Wow! Dziękuję za odpowiedź. Nie spodziewałem się, że dostanę coś dobrego. Właśnie przetestowałem to i wynik, którego szukałem. Teraz muszę po prostu zrozumieć cały twój kod. Myślę, że to zajmuje trochę czasu. Niż mogę go nieco zmodyfikować, aby dopasować dokładnie to, co próbuję osiągnąć. Ale tak naprawdę tego szukałem! – PhilippS

3

Można je łańcuszek,

Kolektor może produkować tylko jeden obiekt, ale ten obiekt może posiadać wiele wartości. Możesz zwrócić mapę, na przykład, gdy mapa ma wpis dla każdego zwracanego kolektora.

Można użyć Collectors.of(HashMap::new, accumulator, combiner);

Twój accumulator miałoby Mapa kolektorów gdzie klucze Mapie produkowanego odpowiada nazwie kolektora. Kombinator ten będzie potrzebował sposobu na łączenie wielu wyników esp, gdy odbywa się to równolegle.


Generalnie wbudowane kolektory wykorzystują typ danych dla złożonych wyników.

z kolektorów

public static <T> 
Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper) { 
    return new CollectorImpl<T, DoubleSummaryStatistics, DoubleSummaryStatistics>(
      DoubleSummaryStatistics::new, 
      (r, t) -> r.accept(mapper.applyAsDouble(t)), 
      (l, r) -> { l.combine(r); return l; }, CH_ID); 
} 

i we własnym klasie

public class DoubleSummaryStatistics implements DoubleConsumer { 
    private long count; 
    private double sum; 
    private double sumCompensation; // Low order bits of sum 
    private double simpleSum; // Used to compute right sum for non-finite inputs 
    private double min = Double.POSITIVE_INFINITY; 
    private double max = Double.NEGATIVE_INFINITY; 
+0

Próbuję dowiedzieć się, jak to może pomóc mojemu. Być może już nie jestem na dobrej drodze. Czy nie jest to tylko jedna wbudowana funkcja, taka jak wspomniana "Collectors.counting()"? Ale chcę to zrobić, użyć dwóch lub więcej z tych wbudowanych funkcji (nieznanych w czasie kompilacji). Może możesz wyjaśnić trochę więcej. – PhilippS

+0

Dzięki za odpowiedź. Zapewnia to dobrą znajomość tła, aby w szczególności zrozumieć odpowiedź Tagirs i dostosować ją. – PhilippS

3

Zamiast łączenia kolektorów, należy zbudować abstrakcję, która jest agregatorem kolektorów: implementuj interfejs Collector z klasą, która akceptuje listę kolektorów i deleguje każde wywołanie metody do każdego z nich. Następnie na koniec zwracasz new Data() ze wszystkimi wynikami, jakie wygenerowały zagnieżdżone kolektory.

Można uniknąć tworzenia niestandardowej klasy ze wszystkimi deklaracjami metody poprzez wykorzystanie Collector.of(supplier, accumulator, combiner, finisher, Collector.Characteristics... characteristics)finisher lambda wywoła finiszer każdego zagnieżdżonego kolektora, a następnie powrócić instancję Data.

+0

Dzięki za odpowiedź. Szczególnie w połączeniu z innymi odpowiedziami zapewnia to dobrą wiedzę na temat tego, jak dostosować mój kod. – PhilippS

4

Używając mapy jako typu wyjściowego, można mieć potencjalnie nieskończoną listę reduktorów, z których każda tworzy własną statystykę i dodaje ją do mapy.

public static <K, V> Map<K, V> addMap(Map<K, V> map, K k, V v) { 
    Map<K, V> mapout = new HashMap<K, V>(); 
    mapout.putAll(map); 
    mapout.put(k, v); 
    return mapout; 
} 

...

List<Person> persons = new ArrayList<>(); 
    persons.add(new Person("Person One", 1, 18)); 
    persons.add(new Person("Person Two", 1, 20)); 
    persons.add(new Person("Person Three", 1, 30)); 
    persons.add(new Person("Person Four", 2, 30)); 
    persons.add(new Person("Person Five", 2, 29)); 
    persons.add(new Person("Person Six", 3, 18)); 

    List<BiFunction<Map<String, Integer>, Person, Map<String, Integer>>> listOfReducers = new ArrayList<>(); 

    listOfReducers.add((m, p) -> addMap(m, "Count", Optional.ofNullable(m.get("Count")).orElse(0) + 1)); 
    listOfReducers.add((m, p) -> addMap(m, "Sum", Optional.ofNullable(m.get("Sum")).orElse(0) + p.i1)); 

    BiFunction<Map<String, Integer>, Person, Map<String, Integer>> applyList 
      = (mapin, p) -> { 
       Map<String, Integer> mapout = mapin; 
       for (BiFunction<Map<String, Integer>, Person, Map<String, Integer>> f : listOfReducers) { 
        mapout = f.apply(mapout, p); 
       } 
       return mapout; 
      }; 
    BinaryOperator<Map<String, Integer>> combineMaps 
      = (map1, map2) -> { 
       Map<String, Integer> mapout = new HashMap<>(); 
       mapout.putAll(map1); 
       mapout.putAll(map2); 
       return mapout; 
      }; 
    Map<String, Integer> map 
      = persons 
      .stream() 
      .reduce(new HashMap<String, Integer>(), 
        applyList, combineMaps); 
    System.out.println("map = " + map); 

Produkuje:

map = {Sum=10, Count=6} 
Powiązane problemy