After creating the nested map via groupingBy
you would normally end up with a list of MyObject
. But you want to sum those values so you need to use CollectingAndThen
with a finisher
. In this case the finisher streams each list and applies a reduction to build the final object. In my solution I simply created a new MySumObject
each time, adding the previouly computed MySumObject, to the next MyObject
value from the list. This method is quite common and has advantages in that it also works with immutable containers such as records
and does not require additional modification to your classes outside of the standard getters used to retrieve the fields. To avoid clutter, I created a helper method to do the reduction. But the actual summation code could have been put in the collector.
Map<String, Map<String, MySumObject>> map = list.stream()
.collect(Collectors.groupingBy(
MyObject::getLoanType,
Collectors.groupingBy(
MyObject::getLoanCurrency,
Collectors.collectingAndThen(
Collectors.toList(),
lst->summing(lst)))));
Helper method to reduce the list to a single object. Since the reduction works on two different types, the three argument version of reduce is used. First the target MySumObject
is initialized with zero sums. Then the reduction creates the new MySumObject
and adds each appropriate MyObject
to it. The third argument would be used for parallel processing to sum the different MySumObjects
from different threads. In this case, I just assigned the already computed value assuming a single stream.
public static MySumObject summing(List<MyObject> list) {
return list.stream()
.reduce(new MySumObject(
BigDecimal.ZERO,
BigDecimal.ZERO),
(MySumObject mySum, MyObject myOb) -> new MySumObject(
myOb.getAmountPaid()
.add(mySum.getPaidSum()),
myOb.getAmountRemaining()
.add(mySum.getRemainingSum())),(a,b)->a);
}
As an alternative, you could also do it like this which I believe would be more efficient since you are summing as each MyObject
is referenced. It takes advantage of the Java 8+ map enhancements. ComputeIfAbsent creates the value if the key is missing. It will return that value or the existing one. Since that value is also a map, compute can be applied. That will also create the value if the key is missing or process the existing value.
Map<String, Map<String, MySumObject>> map = new HashMap<>();
for (MyObject mo : list) {
map.computeIfAbsent(mo.getLoanType(),
v -> new HashMap<String, MySumObject>())
.compute(mo.getLoanCurrency(),
(k, v) -> v == null ?
new MySumObject(
mo.getAmountPaid(),
mo.getAmountRemaining()) :
new MySumObject(
v.getPaidSum().add(mo
.getAmountPaid()),
v.getRemainingSum().add(mo
.getAmountRemaining())));
}
For the following data, the output is shown and would be identical for both methods.
public static void main(String[] args) {
List<MyObject> list = List.of(
new MyObject("Type1", "Currency1",
BigDecimal.valueOf(10),
BigDecimal.valueOf(100)),
new MyObject("Type2", "Currency2",
BigDecimal.valueOf(20),
BigDecimal.valueOf(200)),
new MyObject("Type1", "Currency1",
BigDecimal.valueOf(30),
BigDecimal.valueOf(300)),
new MyObject("Type2", "Currency2",
BigDecimal.valueOf(40),
BigDecimal.valueOf(400)));
map.entrySet().forEach(System.out::println);
prints
Type2={Currency2=MySumObject[getPaidSum=60, getRemainingSum=600]}
Type1={Currency1=MySumObject[getPaidSum=40, getRemainingSum=400]}