Reputation: 908
I have a List fooList
class Foo {
private String category;
private int amount;
private int price;
... constructor, getters & setters
}
I would like to group by category and then sum amount aswell as price.
The result will be stored in a map:
Map<Foo, List<Foo>> map = new HashMap<>();
The key is the Foo holding the summarized amount and price, with a list as value for all the objects with the same category.
So far I've tried the following:
Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));
Now I only need to replace the String key with a Foo object holding the summarized amount and price. Here is where I'm stuck. I can't seem to find any way of doing this.
Upvotes: 22
Views: 43500
Reputation: 6573
My variation of Sweeper's answer uses a reducing Collector instead of streaming twice to sum the individual fields:
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()));
It is not really better though, as it creates a lot of short-lived Foos
.
Note however that Foo
is required to provide hashCode
- and equals
-implementations that take only category
into account for the resulting map
to work correctly. This would probably not be what you want for Foo
s in general. I would prefer defining a separate FooSummary
class to contain the aggregated data.
Upvotes: 9
Reputation: 1670
You can get the sum like this,
ArrayList<Foo> list = new ArrayList<>();
list.add(new Foo("category_1", 10, 20));
list.add(new Foo("category_2", 11, 21));
list.add(new Foo("category_3", 12, 22));
list.add(new Foo("category_1", 13, 23));
list.add(new Foo("category_2", 14, 24));
list.add(new Foo("category_2", 15, 25));
Map<String, Foo> map = list.stream().collect(Collectors.toMap(Foo::getCategory, Function.identity(), (a1, a2) -> {
a1.joiner(a2);
return a1;
}));
Make sure to add this method to Foo class as well,
public Foo joiner(Foo that) {
this.price += that.price;
this.amount += that.amount;
return this;
}
Upvotes: 0
Reputation: 34450
If you have a special, dedicated constructor and hashCode
and equals
methods consistently implemented in Foo
as follows:
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);
}
The hashCode
and equals
implementations above allow you to use Foo
as a meaningful key in the map (otherwise your map would be broken).
Now, with the help of a new method in Foo
that performs the aggregation of the amount
and price
attributes at the same time, you can do what you want in 2 steps. First the method:
public void aggregate(Foo that) {
this.amount += that.amount;
this.price += that.price;
}
Now the final solution:
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: a few observations were missing...
On one hand, this solution forces you to use an implementation of hashCode
and equals
that considers two different Foo
instances as equal if they belong to the same category
. Maybe this is not desired, or you already have an implementation that takes more or other attributes into account.
On the other hand, using Foo
as the key of a map which is used to group instances by one of its attributes is quite an uncommon use case. I think it would be better to just use the category
attribute to group by category and have two maps: Map<String, List<Foo>>
to keep the groups and Map<String, Foo>
to keep the aggregated price
and amount
, with the key being the category
in both cases.
Besides this, this solution mutates the keys of the map after the entries are put into it. This is dangerous, because this could break the map. However, here I'm only mutating attributes of Foo
that don't participate in neither hashCode
nor equals
Foo
's implementation. I think that this risk is acceptable in this case, due to the unusuality of the requirement.
Upvotes: 3
Reputation: 270960
A bit ugly, but it should work:
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));
Upvotes: 26
Reputation: 5463
My take on a solution :)
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);
}
Upvotes: 0
Reputation: 2773
I suggest you create a helper class, which will hold amount and price
final class Pair {
final int amount;
final int price;
Pair(int amount, int price) {
this.amount = amount;
this.price = price;
}
}
And then just collect list to map:
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)));
Upvotes: 2