У меня есть коллекция pojos:
public class Foo {
String name;
String date;
int count;
}
Мне нужно перебрать коллекцию, groupBy Foos по имени и сумме, а затем создать новую коллекцию с pojos с суммированным счетчиком.
Вот как я это делаю сейчас:
List<Foo> foosToSum = ...
Map<String, List<Foo>> foosGroupedByName = foosToSum.stream()
.collect(Collectors.groupingBy(Foo::getName));
List<Foo> groupedFoos = foosGroupedByName.keySet().stream().map(name -> {
int totalCount = 0;
String date = "";
for(Foo foo: foosGroupedByName.get(name)) {
totalCount += foo.getCount();
date = foo.getDate() //last is used
}
return new Foo(name, date, totalCount);
}).collect(Collectors.toList());
Есть ли более красивый способ сделать это с помощью потоков?
ОБНОВИТЬ Спасибо всем за помощь. Все ответы были отличными. Я решил создать функцию слияния в pojo.
Окончательное решение выглядит так:
Collection<Foo> groupedFoos = foosToSum.stream()
.collect(Collectors.toMap(Foo::getName, Function.identity(), Foo::merge))
.values();




Да, вы можете использовать нижний коллектор в вашем groupingBy для немедленного суммирования подсчетов. После этого транслируйте карту и сопоставьте ее с Foos.
foosToSum.stream()
.collect(Collectors.groupingBy(Foo::getName,
Collectors.summingInt(Foo::getCount)))
.entrySet()
.stream()
.map(entry -> new Foo(entry.getKey(), null, entry.getValue()))
.collect(Collectors.toList());
Более эффективное решение могло бы избежать группировки в карту только для немедленной потоковой передачи, но жертвует некоторой удобочитаемостью (на мой взгляд):
foosToSum.stream()
.collect(Collectors.groupingBy(Foo::getName,
Collectors.reducing(new Foo(),
(foo1, foo2) -> new Foo(foo1.getName(), null, foo1.getCount() + foo2.getCount()))))
.values();
Уменьшая Foos вместо int, мы учитываем имя и можем сразу же суммировать в Foo.
Что ж, вы можете попробовать просто использовать foo2.getDate() вместо null. Но тогда вы полагаетесь на то, что foos все еще заказывают. Если вы хотите убедиться, вы можете использовать что-то вроде getLater(foo1.getDate(), foo2.getDate()). Как сказал Аомине в другом ответе, вы можете поместить всю эту логику в класс Foo и написать ...reducing(new Foo(), Foo::merge)
Вы можете сделать это либо с помощью groupingBy, либо с помощью сборщика toMap, вопрос о том, что использовать, остается спорным, поэтому я позволю вам выбрать тот, который вам больше нравится.
Для лучшей читаемости я бы создал функцию слияния в Foo и спрятал всю логику слияния внутри нее.
Это также означает лучшую ремонтопригодность, поскольку чем сложнее становится слияние, вам нужно только изменить одно место, и это метод merge, а не запрос потока.
например
public Foo merge(Foo another){
this.count += another.getCount();
/* further merging if needed...*/
return this;
}
Теперь вы можете:
Collection<Foo> resultSet = foosToSum.stream()
.collect(Collectors.toMap(Foo::getName,
Function.identity(), Foo::merge)).values();
Обратите внимание, что указанная выше функция слияния изменяет объекты в исходной коллекции, если вместо этого вы хотите сохранить ее неизменной, вы можете создать новые Foo следующим образом:
public Foo merge(Foo another){
return new Foo(this.getName(), null, this.getCount() + another.getCount());
}
Кроме того, если по какой-то причине вам явно требуется List<Foo> вместо Collection<Foo>, то это можно сделать с помощью конструктора копирования ArrayList.
List<Foo> resultList = new ArrayList<>(resultSet);
Обновлять
Как упоминал @Federico в комментариях, последняя функция слияния выше стоит дорого, поскольку создает ненужные объекты, которых можно было бы избежать. Итак, как он предложил, более удобной альтернативой является использование первой функции слияния, которую я показал выше, а затем изменение запроса к потоку на это:
Collection<Foo> resultSet = foosToSum.stream()
.collect(Collectors.toMap(Foo::getName,
f -> new Foo(f.getName(), null, f.getCount()), Foo::merge))
.values();
Привет, Аомине! Просто комментарий ... Если вы используете Function.identity() в качестве функции преобразования значений, правильно, что элементы исходной коллекции изменены. Итак, вы предлагаете создать новые элементы Foo при слиянии. Хотя это работает и правильно, оно создает ненужные объекты. Вы можете создать копию в функции сопоставления значений (просто используйте конструктор копирования), а затем объединить эту копию, как в исходном методе merge.
Привет, @FedericoPeraltaSchaffner. Я видел ваш комментарий ранее, но не мог ответить. сейчас я :) . в любом случае, вы правы, и я соответствующим образом обновил свой ответ. Спасибо за ответ.
Привет, Аомине, здорово, ты обновил свой уже принятый ответ;) Я уже проголосовал заранее :)
@FedericoPeraltaSchaffner на самом деле, глядя на мой код еще раз, он создавал новые объекты только тогда, когда происходит столкновение ключей, и в случае, если столкновения нет, он все равно будет поддерживать этот конкретный объект из источника. так что вы не только улучшили мой код, но и решили тонкую проблему :).
Аомине, да, и в этом вы правы. Я этого не заметил. Тогда хорошо. Мы убили двух зайцев одним выстрелом ...
@FedericoPeraltaSchaffner, зачем создавать новые объекты, если мы можем видоизменить и повторно использовать старые. Таким образом, функция слияния будет выглядеть как исходная, а Function.identity() останется.
@KirillBazarov, если можно изменять существующие объекты, вам не нужно создавать новые объекты. Я предложил создавать новые объекты только в том случае, если вы не хотите изменять существующие объекты.
Хорошо, теперь я понял. Спасибо.
В моем случае нет никакого параллельного выполнения, и сбор осуществляется в локальной области, поэтому я считаю, что повторно использовать одни и те же объекты безопасно.
Я не понимаю, чем ваша вторая (обновленная) версия лучше первой. Я чувствую, что только во втором случае вы будете каждый раз создавать один объект Foo (valueMapper в toMap) - в то время как в первом вы используете Function.identity и выполняете слияние Только при конфликте ключей. Я что-то пропустил?
@ user7 Дело в том, что функция слияния лучше в обновленной версии. Но что использовать, Function.identity или новый объект - решать вам, в зависимости от того, нужны ли вам новые объекты или изменить существующие. Первая функция слияния создаст ненужные объекты, если, например, существует более одного конфликта для одного и того же ключа.
Отличное решение. Но мне нужно как-то получить поле последней даты, сгруппированное по имени Foos. Моя ошибка, я написал, что это не важно, но это важно.