Java stream группирует и суммирует несколько полей

у меня есть fooList список

class Foo {
    private String category;
    private int amount;
    private int price;

    ... constructor, getters & setters
}

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

результат будет сохранен на карте:

Map<Foo, List<Foo>> map = new HashMap<>();

ключ-это Foo, содержащий суммированную сумму и цену, со списком в качестве значения для всех объектов с той же категорией.

до сих пор я пробовал следующие:

Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));

Теперь мне нужно только заменить строковый ключ на объект Foo, содержащий суммированное количество и цена. Вот где я застрял. Я не могу найти никакого способа сделать это.

5 ответов


немного уродливый, но он должен работать:

list.stream().collect(Collectors.groupingBy(Foo::getCategory))
    .entrySet().stream()
    .collect(Collectors.toMap(x -> {
        int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
        int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
        return new Foo(x.getKey(), sumAmount, sumPrice);
    }, Map.Entry::getValue));

мой вариант Sweepers ответ использует уменьшающий коллектор вместо потоковой передачи дважды для суммирования отдельных полей:

        Map<Foo, List<Foo>> map = fooList.stream()
                .collect(Collectors.groupingBy(Foo::getCategory))
                .entrySet().stream()
                .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                            Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                                        l.getAmount() + r.getAmount(),
                                        l.getPrice() + r.getPrice())))
                            .get(), 
                            e -> e.getValue()));

это не правда лучше хотя, как это создает много недолговечных Foos.

обратите внимание, однако, что Foo обязана предоставить hashCode и equals-реализаций, которые принимают только category в результате map для правильной работы. Это, вероятно, не то, чего ты хочешь. для Fooв целом. Я бы предпочел определить отдельный FooSummary класс для хранения агрегированных данных.


если у вас есть специальная конструктор и hashCode и equals методы последовательно реализованы в Foo следующим образом:

public Foo(Foo that) { // not a copy constructor!!!
    this.category = that.category;
    this.amount = 0;
    this.price = 0;
}

public int hashCode() {
    return Objects.hashCode(category);
}

public boolean equals(Object another) {
   if (another == this) return true;
   if (!(another instanceof Foo)) return false;
   Foo that = (Foo) another;
   return Objects.equals(this.category, that.category);
}

на hashCode и equals реализации выше позволяют использовать Foo как значимый ключ на карте (в противном случае ваша карта будет нарушен).

теперь, с помощью нового метода в Foo который выполняет агрегацию amount и price атрибуты, в то же время, вы можете делать то, что вы хотите в 2 лестница. Сначала метод:

public void aggregate(Foo that) {
    this.amount += that.amount;
    this.price += that.price;
}

теперь окончательное решение:

Map<Foo, List<Foo>> result = fooList.stream().collect(
    Collectors.collectingAndThen(
        Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
        m -> { m.forEach((k, v) -> v.forEach(k::aggregate)); return m; }));

EDIT: несколько наблюдений отсутствовали...

С одной стороны, это решение заставляет вас использовать реализацию hashCode и equals что считает два разных Foo экземпляры равны, если они принадлежат одному и тому же category. Возможно, это нежелательно, или у вас уже есть реализация, которая учитывает больше или другие атрибуты.

на другая рука, используя Foo как ключ карты, которая используется для группировки экземпляров по одному из ее атрибутов, является довольно необычным случаем использования. Я думаю, что было бы лучше просто использовать category атрибут группы по категориям и имеют две карты: Map<String, List<Foo>> сохранить группы и Map<String, Foo> сохранить агрегированные price и amount, С ключом, являющимся category в обоих случаях.

кроме того, это решение мутирует ключи карты после ввода в нее записей. Этот это опасно, потому что это может повредить карте. Однако здесь я только мутирую атрибуты Foo которые не участвуют ни в одном hashCode, ни equals 'ы. Я думаю, что этот риск приемлем в данном случае, из-за необычности требования.


Я предлагаю вам создать вспомогательный класс, который будет содержать сумму и цена

final class Pair {
    final int amount;
    final int price;

    Pair(int amount, int price) {
        this.amount = amount;
        this.price = price;
    }
}

а затем просто собрать список на карту:

List<Foo> list =//....;

Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                    foo -> new Pair(foo.getAmount(), foo.getPrice()),
                    (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));

мое решение :)

public static void main(String[] args) {
    List<Foo> foos = new ArrayList<>();
    foos.add(new Foo("A", 1, 10));
    foos.add(new Foo("A", 2, 10));
    foos.add(new Foo("A", 3, 10));
    foos.add(new Foo("B", 1, 10));
    foos.add(new Foo("C", 1, 10));
    foos.add(new Foo("C", 5, 10));

    List<Foo> summarized = new ArrayList<>();
    Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>() {
        @Override
        public Foo apply(Foo t) {
            Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
            Foo f;
            if (!fOpt.isPresent()) {
                f = new Foo(t.getCategory(), 0, 0);
                summarized.add(f);
            } else {
                f = fOpt.get();
            }
            f.setAmount(f.getAmount() + t.getAmount());
            f.setPrice(f.getPrice() + t.getPrice());
            return f;
        }
    }));
    System.out.println(collect);
}