I have a simple class Foo:
class Foo {
String type;
Integer quantity;
String code;
//...
}
and a list of objects of that type:
{ type=11, quantity=1, code=A },
{ type=11, quantity=2, code=A },
{ type=11, quantity=1, code=B },
{ type=11, quantity=3, code=C },
{ type=12, quantity=2, code=A },
{ type=12, quantity=1, code=B },
{ type=11, quantity=1, code=B },
{ type=13, quantity=1, code=C },
{ type=13, quantity=3, code=C }
I need to first group by type, and I can do that:
Map<String, List<Foo>> typeGroups = mylist.stream().collect(Collectors.groupingBy(Foo::getType));
The next step would be transforming that Map
to a List<Foo>
like this:
{ type=11, quantity=3, code=A },
{ type=11, quantity=2, code=B },
{ type=11, quantity=3, code=C },
{ type=12, quantity=2, code=A },
{ type=12, quantity=1, code=B },
{ type=13, quantity=4, code=C }
In SQL, I'd have this kind of query:
SELECT type, code, SUM(quantity) FROM Foo GROUP BY type, code
Let me explain in plain english, too.
I need to end up with a List of Foos. Each one's code must be unique among its group, and quantity must be the aggregate sum of Foos having same code among their group.
I tried doing that using groupingBy()
with summingInt()
but can't get the desired result.
You need to use a groupingBy
collector that groups according to a list made by the type
and the code
. This works because two lists are equal when all of their elements are equal and in the same order. (Note that the list here only serves as a container of values with an equality defined as equality of all the values contained and in the same order; a custom Tuple
class would also work here).
The following code first creates an intermediate map Map<List<String>, Integer>
where the key corresponds to a list of type and code and the value corresponds to the sum of all the quantities for those type and code. That map is then post-processed: each entry are mapped to a Foo
where the type
is the first value of the key, the quantity
is the value and the code
is the second value of the key.
Map<List<String>, Integer> map =
myList.stream()
.collect(Collectors.groupingBy(
f -> Arrays.asList(f.getType(), f.getCode()),
Collectors.summingInt(Foo::getQuantity)
));
List<Foo> result =
map.entrySet()
.stream()
.map(e -> new Foo(e.getKey().get(0), e.getValue(), e.getKey().get(1)))
.collect(Collectors.toList());
Notice that the above solution is using 2 different Stream pipelines. This could be done using a single Stream pipeline but it would require a collector that is capable of collecting into 2 different collectors. In this case, the first collector would just keep the first grouped value and the second one would sum the quantities. Unfortunately, there are no such collectors in the Stream API itself. This can be done using the StreamEx library that contains such a collector.
In the following code, the input list is still grouped by a list formed by the type and code of the Foo
. What changes is the collector: we're using pairing(c1, c2, finisher)
that pairs two collectors and applies the given finisher operation on the result of the collectors.
first()
that returns the first grouped Foo
(we don't need the others as we know they will have the same type and code). It is wrapped with a call to collectingAndThen
to extract the Optional
value out of the first()
collector (first()
returns an Optional
to handle the case where the stream is empty and would collect into an empty Optional
; this is not possible here so we can call get()
safely).summingInt
.Foo
returned by the first collector and the result of the summation into a new Foo
with the new quantity.Finally, we only need to keep the values of the resulting map. Since Map.values()
returns a Collection
, it is wrapped into a ArrayList
to have the final result.
List<Foo> result = new ArrayList<>(
myList.stream()
.collect(Collectors.groupingBy(
f -> Arrays.<String>asList(f.getType(), f.getCode()),
MoreCollectors.pairing(
Collectors.collectingAndThen(MoreCollectors.first(), Optional::get),
Collectors.summingInt(Foo::getQuantity),
(f, s) -> new Foo(f.getType(), s, f.getCode())
)
)).values());
I think this can be done as follows in one go, assuming
public class Foo {
private String type;
private int quantity;
private String code;
public Foo(String type, int quantity, String code) {
this.type = type;
this.code = code;
this.quantity = quantity;
}
public String getType() {return type;}
public int getQuantity() {return quantity;}
public String getCode() {return code;}
}
and
List<Foo> foos = new ArrayList<>();
foos.add(new Foo("11", 1, "A"));
foos.add(new Foo("11", 2, "A"));
foos.add(new Foo("11", 1, "B"));
foos.add(new Foo("11", 3, "C"));
foos.add(new Foo("12", 2, "A"));
foos.add(new Foo("12", 1, "B"));
foos.add(new Foo("11", 1, "B"));
foos.add(new Foo("13", 1, "C"));
foos.add(new Foo("13", 3, "C"));
The following
foos.stream().collect(
toMap(
f -> f.getType() + f.getCode(),
Function.identity(),
(s, a) -> new Foo(
s.getType(),
s.getQuantity() + a.getQuantity(),
s.getCode()))
)
.values();
produces
Foo[type='12'|quantity=2|code='A']
Foo[type='13'|quantity=4|code='C']
Foo[type='12'|quantity=1|code='B']
Foo[type='11'|quantity=3|code='A']
Foo[type='11'|quantity=2|code='B']
Foo[type='11'|quantity=3|code='C']
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With