Несколько агрегатных функций в Java 8 Stream API

у меня есть класс, определенный как

public class TimePeriodCalc {
    private double occupancy;
    private double efficiency;
    private String atDate;
}

Я хотел бы выполнить следующую инструкцию SQL, используя Java 8 Stream API.

SELECT atDate, AVG(occupancy), AVG(efficiency)
FROM TimePeriodCalc
GROUP BY atDate

пробовал :

Collection<TimePeriodCalc> collector = result.stream().collect(groupingBy(p -> p.getAtDate(), ....

что можно поместить в код для выбора нескольких атрибутов ? Я думаю об использовании нескольких коллекционеров, но на самом деле не знаю, как это сделать.

4 ответов


сделать это без таможни Collector (не потоковое снова на результат), вы можете сделать это так. Это немного грязно, так как он сначала собирает Map<String, List<TimePeriodCalc>> и этот список и получите в среднем вдвое.

так как вам нужно два средних, они собираются в Holder или Pair в данном случае я использую AbstractMap.SimpleEntry

  Map<String, SimpleEntry<Double, Double>> map = Stream.of(new TimePeriodCalc(12d, 10d, "A"), new TimePeriodCalc(2d, 16d, "A"))
            .collect(Collectors.groupingBy(TimePeriodCalc::getAtDate,
                    Collectors.collectingAndThen(Collectors.toList(), list -> {
                        double occupancy = list.stream().collect(
                                Collectors.averagingDouble(TimePeriodCalc::getOccupancy));
                        double efficiency = list.stream().collect(
                                Collectors.averagingDouble(TimePeriodCalc::getEfficiency));
                        return new AbstractMap.SimpleEntry<>(occupancy, efficiency);
                    })));

    System.out.println(map);

предполагая, что ваш TimePeriodCalc класс имеет все необходимые геттеры, это должно получить вам список, который вы хотите:

List<TimePeriodCalc> result = new ArrayList<>(
    list.stream()
    .collect(Collectors.groupingBy(TimePeriodCalc::getAtDate, 
        Collectors.collectingAndThen(Collectors.toList(), TimePeriodCalc::avgTimePeriodCalc)))
    .values()
);

здесь TimePeriodCalc.avgTimePeriodCalc этот способ в TimePeriodCalc класс:

public static TimePeriodCalc avgTimePeriodCalc(List<TimePeriodCalc> list){
    return new TimePeriodCalc(
            list.stream().collect(Collectors.averagingDouble(TimePeriodCalc::getOccupancy)),
            list.stream().collect(Collectors.averagingDouble(TimePeriodCalc::getEfficiency)),
            list.get(0).getAtDate()
            );
}

вышесказанное можно объединить в это чудовище:

List<TimePeriodCalc> result = new ArrayList<>(
    list.stream()
    .collect(Collectors.groupingBy(TimePeriodCalc::getAtDate, 
        Collectors.collectingAndThen(
            Collectors.toList(), a -> {
                return new TimePeriodCalc(
                        a.stream().collect(Collectors.averagingDouble(TimePeriodCalc::getOccupancy)),
                        a.stream().collect(Collectors.averagingDouble(TimePeriodCalc::getEfficiency)),
                        a.get(0).getAtDate()
                        );
            }
        )))
    .values());

входной сигнал:

List<TimePeriodCalc> list = new ArrayList<>();
list.add(new TimePeriodCalc(10,10,"a"));
list.add(new TimePeriodCalc(10,10,"b"));
list.add(new TimePeriodCalc(10,10,"c"));
list.add(new TimePeriodCalc(5,5,"a"));
list.add(new TimePeriodCalc(0,0,"b"));

это даст:

TimePeriodCalc [occupancy=7.5, efficiency=7.5, atDate=a]
TimePeriodCalc [occupancy=5.0, efficiency=5.0, atDate=b]
TimePeriodCalc [occupancy=10.0, efficiency=10.0, atDate=c]

вот способ с пользовательским коллектор. Для этого нужен только один проход, но это не очень легко, особенно из-за дженериков...

если у вас есть этот метод:

@SuppressWarnings("unchecked")
@SafeVarargs
static <T, A, C extends Collector<T, A, Double>> Collector<T, ?, List<Double>>
averagingManyDoubles(ToDoubleFunction<? super T>... extractors) {

    List<C> collectors = Arrays.stream(extractors)
        .map(extractor -> (C) Collectors.averagingDouble(extractor))
        .collect(Collectors.toList());

    class Acc {
        List<A> averages = collectors.stream()
            .map(c -> c.supplier().get())
            .collect(Collectors.toList());

        void add(T elem) {
            IntStream.range(0, extractors.length).forEach(i ->
                collectors.get(i).accumulator().accept(averages.get(i), elem));
        }

        Acc merge(Acc another) {
            IntStream.range(0, extractors.length).forEach(i ->
                averages.set(i, collectors.get(i).combiner()
                    .apply(averages.get(i), another.averages.get(i))));
            return this;
        }

        List<Double> finish() {
            return IntStream.range(0, extractors.length)
                .mapToObj(i -> collectors.get(i).finisher().apply(averages.get(i)))
                .collect(Collectors.toList());
        }
    }
    return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finish);
}

Это получает массив функций, которые будут извлекать double значения из каждого элемента потока. Эти экстракторы преобразуются в Collectors.averagingDouble коллекторы, а затем местные Acc класс создается с изменяемыми структурами, которые используются для накопления средних значений для каждого коллектора. Затем, функция аккумулятора вперед к каждому аккумулятору, и так с функциями комбайнера и финишера.

использование следующим образом:

Map<String, List<Double>> averages = list.stream()
    .collect(Collectors.groupingBy(
        TimePeriodCalc::getAtDate,
        averagingManyDoubles(
            TimePeriodCalc::getOccupancy,
            TimePeriodCalc::getEfficiency)));

вы можете связать несколько атрибутов такой:

Collection<TimePeriodCalc> collector = result.stream().collect(Collectors.groupingBy(p -> p.getAtDate(), Collectors.averagingInt(p -> p.getOccupancy())));

Если вы хотите больше, вы получаете идею.