javajava-streamcollectorsgroupingby

Creating Map of Maps using a custom Object as a reduction type in Java 8


I have a List<MyObject>

class MyObject {
    String loanType;
    String loanCurrency;
    BigDecimal amountPaid;
    BigDecimal amountRemaining;
}

And I need to convert this list into a map Map<String, Map<String, MySumObject>.

I've created a custom MySumObject class because I need to get a Sum for both amountPaid and amountRemaining from the list of MyObject based on the loanType and loanCurrency.

class MySumObject {
    BigDecimal paidSum;
    BigDecimal remainingSum;
}

Using the code below, I can obtain a Map<String,Map<String, BigDecimal>>

Map<String, Map<String, BigDecimal>> result = list1.stream().collect(
    Collectors.groupingBy(LoanObject::getLoanType,
        Collectors.groupingBy(LoanObject::getLoanCurrency,
            Collectors.reducing(
                BigDecimal.ZERO,
                LoanObject::getAmountPaid,
                BigDecimal::add)
        )
    ));

But I'm stuck on changing it to use MySumObject instead of BigDecimal.


Solution

  • Using MySumObject as an accumulation type would make sense only if all the payment operation represented by MyObject in the source list belong to different loans.

    Otherwise, there's a flow in your logic - remaining amount of a particular loan decreases after each payment. It's pointless to calculate the sum of remaining amounts of the same loan. Instead, we can grab the data regarding remaining amount from the loan object with the latest timestamp or having the lowest remaining amount, but that requires different strategy of grouping and different structure of loan object (MyObject), i.e. should have at least loanId.

    That said, correctness of your logic - is your responsibility.

    Let's get back to the initial idea of accumulating loans having the same currency and loan type into MySumObject (I assume that you know what you're doing, and it does make sense).

    It can be achieved by creating a custom collector based on the MySumObject:

    Map<String, Map<String, MySumObject>> result = list1.stream()
        .collect(Collectors.groupingBy(
            MyObject::getLoanType,
            Collectors.groupingBy(
                MyObject::getLoanCurrency,
                    Collector.of(
                        MySumObject::new,
                        MySumObject::addLoan,
                        MySumObject::merge
                    ))
            ));
    

    MySumObject with methods addLoan() and merge().

    public static class MySumObject {
        private BigDecimal paidSum = BigDecimal.ZERO;
        private BigDecimal remainingSum = BigDecimal.ZERO;
        
        public void addLoan(LoanObject loan) {
            paidSum = paidSum.add(loan.getAmountPaid());
            remainingSum = remainingSum.add(loan.getAmountRemaining());
        }
        
        public MySumObject merge(MySumObject other) {
            paidSum = paidSum.add(other.getPaidSum());
            remainingSum = remainingSum.add(other.getRemainingSum());
            return this;
        }
        
        // getters, constructor, etc. are omitted
    }
    

    Sidenote: it doesn't seem to be justifiable to use a string to represent a currency (unless it's not an assignment requirement) because since Java 1.4 we have a class Currency.