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