Группировать и суммировать объекты, как в 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));

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


person haisi    schedule 13.10.2014    source источник


Ответы (4)


Использование 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));
person Holger    schedule 13.10.2014
comment
Хорошо, на самом деле я никогда не использовал эти методы Collectors. Это должен быть принятый ответ - person Dici; 14.10.2014
comment
@Holger Как это сделать в Java 7, пожалуйста? - person hamza-don; 01.06.2015
comment
@don-kaotic: это совсем другой вопрос - person Holger; 02.06.2015
comment
@hamza-don Я думаю, вы уже знаете, что это невозможно в Java 7. - person Sayantan; 23.11.2017
comment
В моем случае у меня есть класс с именем XYZ и список элементов типа Foo. Я хочу groupBy в соответствии с приведенной выше логикой, а затем мне нужно заменить его списком Foo. Поэтому мне нужно сделать что-то вроде xyz.setFooList(performAboveOperation(xyz.getFooList())). Мне нужно заменить список foo на сокращенный список foo в xyz. Любое предложение. Как мне собрать результат в список вместо вызова forEach в этом потоке. - person doga; 17.04.2018
comment
@doga Я думаю, вам следует задать новый вопрос, включая то, что вы пробовали, и обратную ссылку на эти вопросы и ответы, если хотите, чтобы предоставить больше контекста. - person Holger; 17.04.2018

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

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)]
person Dici    schedule 13.10.2014
comment
Если я правильно понимаю, вам нужно создавать новый объект Foo для каждой операции сокращения, потому что в противном случае сокращение не подходит для параллельной операции. Однако это пустая трата ресурсов, так как мы можем изменить объект foo на месте. Как вы думаете? Может ли reduce((f1,f2) -> { f1.targetCost += f2.targetCost; f1.actualCost += f2.actualCost; return f1;}) работать? - person Sobvan; 19.07.2017
comment
Общее правило при использовании функционального стиля заключается в том, что функции должны быть чистыми, то есть без каких-либо побочных эффектов. Создание новой ссылки каждый раз имеет небольшую стоимость, которая должна быть незначительной для подавляющего большинства приложений. Если вас действительно беспокоит производительность, не используйте потоки, так как они создают дополнительные затраты по сравнению с простым циклом. - person Dici; 19.07.2017
comment
Спасибо @Dici. Прочитав немного больше об этой теме, я обнаружил, что stream(). collect() вместо stream().reduce() я не хочу создавать новый объект на каждой итерации. Эта статья весьма полезна для понимания функции collect(): javabrahman.com/java-8/ - person Sobvan; 19.07.2017

Делать это только с помощью JDK Stream API не так просто, как показали другие ответы. В этой статье объясняется, как добиться семантики 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]]
person Lukas Eder    schedule 27.08.2015
comment
Просто подсказка, если у вас уже есть список, вы можете пройти Seq.of(yourList.toArray()).ofType(YourListType.class) ... - person Rodolfo Faquin; 27.03.2020
comment
@RodolfoFaquin: Зачем тебе это? - person Lukas Eder; 27.03.2020
comment
Например, если у вас есть List<YourListType>, заполненные запросом, и вам нужно их сгруппировать, вы можете сделать это, как в моем примере. У вас есть другие советы, как это сделать? - person Rodolfo Faquin; 28.03.2020
comment
@RodolfoFaquin Просто используйте Seq.seq(list) - person Lukas Eder; 28.03.2020

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(), очень просто

person user1241671    schedule 15.12.2016