it-swarm.com.ru

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

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

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

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

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

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

35
haisi

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

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

  • Подсчет:

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

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

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)]
13
Dici
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
user1241671

Делать это с помощью API Stream в JDK не очень просто, как показали другие ответы. В этой статье объясняется, как можно достичь семантики 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]]
1
Lukas Eder
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));
}
0
Shylock.Gou