Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 Sum two object properties in one iteration

I have a List<LedgerEntry> ledgerEntries and I need to calculate the sums of creditAmount and debitAmount.

class LedgerEntry{
 private BigDecimal creditAmount;
 private BigDecimal debitAmount;

 //getters and setters
}

I have implemented this as,

BigDecimal creditTotal = ledgeredEntries.stream().map(p ->p.getCreditAmount()).
reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal debitTotal = ledgeredEntries.stream().map(p ->p.getDebitAmount()).
reduce(BigDecimal.ZERO, BigDecimal::add);

//...
//Use creditTotal, debitTotal later

This looks like I'm iterating over the List twice. Is there a way to get this done in one go without having to steam the list twice?

Pre Java 8 version

BigDecimal creditTotal = BigDecimal.ZERO;
BigDecimal debitTotal = BigDecimal.ZERO;
for(LedgerEntry entry : ledgerEntries){
  creditTotal = creditTotal.add(entry.getCreditAmount());
  debitTotal = debitTotal.add(entry.getDebitAmount());
}
like image 500
Krishan Avatar asked Feb 22 '17 08:02

Krishan


2 Answers

You could reduce to a totals entry:

LedgerEntry totalsEntry = entries.stream().reduce(new LedgerEntry(), (te, e) -> {
    te.setCreditAmount(te.getCreditAmount().add(e.getCreditAmount()));
    te.setDebitAmount(te.getDebitAmount().add(e.getDebitAmount()));

    return te;
});

Update

In the comments it was correctly pointed out that reduce() should not modify the initial identifier value, and that collect() should be used for mutable reductions. Below is a version using collect() (using the same BiConsumer as both accumulator and combiner). It also addresses the issue of potential NPEs if the creditAmount and/or debitAmount values have not been set.

BiConsumer<LedgerEntry, LedgerEntry> ac = (e1, e2) -> {
    BigDecimal creditAmount = e1.getCreditAmount() != null ? e1.getCreditAmount() : BigDecimal.ZERO;
    BigDecimal debitAmount = e1.getDebitAmount() != null ? e1.getDebitAmount() : BigDecimal.ZERO;

    e1.setCreditAmount(creditAmount.add(e2.getCreditAmount()));
    e1.setDebitAmount(debitAmount.add(e2.getDebitAmount()));
};

LedgerEntry totalsEntry = entries.stream().collect(LedgerEntry::new, ac, ac);

All of the sudden the pre-Java 8 version is starting to look mighty attractive.

like image 103
Robby Cornelissen Avatar answered Oct 28 '22 15:10

Robby Cornelissen


You need to wrap your results into a Pair of some sort:

stream
        .parallel()
        .reduce(new AbstractMap.SimpleEntry<>(BigDecimal.ZERO, BigDecimal.ZERO),
                    (entry, ledger) -> {
                        BigDecimal credit = BigDecimal.ZERO.add(entry.getKey()).add(ledger.getCreditAmount());
                        BigDecimal debit = BigDecimal.ZERO.add(entry.getValue()).add(ledger.getDebitAmount());
                        return new AbstractMap.SimpleEntry<>(credit, debit);
                    }, (left, right) -> {
                        BigDecimal credit = BigDecimal.ZERO.add(left.getKey()).add(right.getKey());
                        BigDecimal debit = BigDecimal.ZERO.add(left.getValue()).add(right.getValue());
                        return new AbstractMap.SimpleEntry<>(credit, debit);
                    }));
like image 21
Eugene Avatar answered Oct 28 '22 15:10

Eugene