Подтвердить что ты не робот

Группировать по и суммировать объекты, как в SQL с Java lambdas?

У меня есть класс Foo с этими полями:

id: int/name; String/targetCost: BigDecimal/actualCost: BigDecimal

Я получаю arraylist объектов этого класса. например:

new Foo(1, "P1", 300, 400), 
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 30, 20),
new Foo(3, "P3", 70, 20),
new Foo(1, "P1", 360, 40),
new Foo(4, "P4", 320, 200),
new Foo(4, "P4", 500, 900)

Я хочу преобразовать эти значения, создав сумму "targetCost" и "actualCost" и группируя "строку", например.

new Foo(1, "P1", 660, 440),
new Foo(2, "P2", 600, 400),
new Foo(3, "P3", 100, 40),
new Foo(4, "P4", 820, 1100)

То, что я написал сейчас:

data.stream()
       .???
       .collect(Collectors.groupingBy(PlannedProjectPOJO::getId));

Как я могу это сделать?

4b9b3361

Ответ 1

Использование Collectors.groupingBy - правильный подход, но вместо использования единственной версии аргумента, которая создаст список всех элементов для каждой группы, вы должны использовать два arg, которая принимает еще один Collector, который определяет, как агрегировать элементы каждой группы.

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

  • Counting:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id, Collectors.counting()))
      .forEach((id,count)->System.out.println(id+"\t"+count));
    
  • Подводя итог одному свойству:

    list.stream()
      .collect(Collectors.groupingBy(foo -> foo.id,
                                        Collectors.summingInt(foo->foo.targetCost)))
      .forEach((id,sumTargetCost)->System.out.println(id+"\t"+sumTargetCost));
    

В вашем случае, если вы хотите агрегировать более одного свойства, определяющего операцию пользовательского сокращения как это предлагается в этом ответе, это правильный подход, однако вы можете выполнить справа в процессе группировки, поэтому нет необходимости собирать все данные в Map<…,List> перед выполнением сокращения:

(Я предполагаю, что вы используете import static java.util.stream.Collectors.*; сейчас...)

list.stream().collect(groupingBy(foo -> foo.id, collectingAndThen(reducing(
  (a,b)-> new Foo(a.id, a.ref, a.targetCost+b.targetCost, a.actualCost+b.actualCost)),
      Optional::get)))
  .forEach((id,foo)->System.out.println(foo));

Для полноты, здесь решение проблемы выходит за рамки вашего вопроса: что, если вы хотите GROUP BY несколько столбцов/свойств?

Первое, что бросается в голову программистам, - это использовать groupingBy для извлечения свойств элементов потоков и создания/возврата нового ключевого объекта. Но для этого требуется соответствующий класс владельца для ключевых свойств (и Java не имеет класса Tuple общего назначения).

Но есть альтернатива. Используя трехмерную форму groupingBy, мы можем указать поставщика для фактической реализации Map, которая определит равенство ключа. Используя отсортированную карту с компаратором, сравнивая несколько свойств, мы получаем желаемое поведение без необходимости в дополнительном классе. Мы должны позаботиться о том, чтобы не использовать свойства из ключевых экземпляров, которые проигнорировал наш компаратор, поскольку они будут иметь только произвольные значения:

list.stream().collect(groupingBy(Function.identity(),
  ()->new TreeMap<>(
    // we are effectively grouping by [id, actualCost]
    Comparator.<Foo,Integer>comparing(foo->foo.id).thenComparing(foo->foo.actualCost)
  ), // and aggregating/ summing targetCost
  Collectors.summingInt(foo->foo.targetCost)))
.forEach((group,targetCostSum) ->
    // take the id and actualCost from the group and actualCost from aggregation
    System.out.println(group.id+"\t"+group.actualCost+"\t"+targetCostSum));

Ответ 2

Вот один из возможных подходов:

public class Test {
    private static class Foo {
        public int id, targetCost, actualCost;
        public String ref;

        public Foo(int id, String ref, int targetCost, int actualCost) {
            this.id = id;
            this.targetCost = targetCost;
            this.actualCost = actualCost;
            this.ref = ref;
        }

        @Override
        public String toString() {
            return String.format("Foo(%d,%s,%d,%d)",id,ref,targetCost,actualCost);
        }
    }

    public static void main(String[] args) {
        List<Foo> list = Arrays.asList(
            new Foo(1, "P1", 300, 400), 
            new Foo(2, "P2", 600, 400),
            new Foo(3, "P3", 30, 20),
            new Foo(3, "P3", 70, 20),
            new Foo(1, "P1", 360, 40),
            new Foo(4, "P4", 320, 200),
            new Foo(4, "P4", 500, 900));

        List<Foo> transform = list.stream()
            .collect(Collectors.groupingBy(foo -> foo.id))
            .entrySet().stream()
            .map(e -> e.getValue().stream()
                .reduce((f1,f2) -> new Foo(f1.id,f1.ref,f1.targetCost + f2.targetCost,f1.actualCost + f2.actualCost)))
                .map(f -> f.get())
                .collect(Collectors.toList());
        System.out.println(transform);
    }
}

Выход:

[Foo(1,P1,660,440), Foo(2,P2,600,400), Foo(3,P3,100,40), Foo(4,P4,820,1100)]

Ответ 3

data.stream().collect(toMap(foo -> foo.id,
                       Function.identity(),
                       (a, b) -> new Foo(a.getId(),
                               a.getNum() + b.getNum(),
                               a.getXXX(),
                               a.getYYY()))).values();

просто используйте toMap(), очень просто

Ответ 4

Выполнение этого с помощью API JDK Stream только не так просто, как показали другие ответы. В этой статье объясняется, как вы можете достичь семантики SQL GROUP BY в Java 8 (со стандартными агрегатными функциями) и используя jOOλ, библиотека, которая расширяет Stream для этих прецедентов.

Запись:

import static org.jooq.lambda.tuple.Tuple.tuple;

import java.util.List;
import java.util.stream.Collectors;

import org.jooq.lambda.Seq;
import org.jooq.lambda.tuple.Tuple;
// ...

List<Foo> list =

// FROM Foo
Seq.of(
    new Foo(1, "P1", 300, 400),
    new Foo(2, "P2", 600, 400),
    new Foo(3, "P3", 30, 20),
    new Foo(3, "P3", 70, 20),
    new Foo(1, "P1", 360, 40),
    new Foo(4, "P4", 320, 200),
    new Foo(4, "P4", 500, 900))

// GROUP BY f1, f2
.groupBy(
    x -> tuple(x.f1, x.f2),

// SELECT SUM(f3), SUM(f4)
    Tuple.collectors(
        Collectors.summingInt(x -> x.f3),
        Collectors.summingInt(x -> x.f4)
    )
)

// Transform the Map<Tuple2<Integer, String>, Tuple2<Integer, Integer>> type to List<Foo>
.entrySet()
.stream()
.map(e -> new Foo(e.getKey().v1, e.getKey().v2, e.getValue().v1, e.getValue().v2))
.collect(Collectors.toList());

Вызов

System.out.println(list);

Тогда получим

[Foo [f1=1, f2=P1, f3=660, f4=440],
 Foo [f1=2, f2=P2, f3=600, f4=400], 
 Foo [f1=3, f2=P3, f3=100, f4=40], 
 Foo [f1=4, f2=P4, f3=820, f4=1100]]

Ответ 5

public  <T, K> Collector<T, ?, Map<K, Integer>> groupSummingInt(Function<? super T, ? extends K>  identity, ToIntFunction<? super T> val) {
    return Collectors.groupingBy(identity, Collectors.summingInt(val));
}