Java 8 Sum two object properties in one iteration
Asked Answered
P

3

7

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());
}
Patricio answered 22/2, 2017 at 8:25 Comment(4)
Why do you want to use streams? Your "Pre Java 8" version is also 100% valid Java 8 and (when fixed for the fact that it doesn't actually do anything because BigDecimals are immutable) more readable and maintainable (and probabably more performant) than any stream solution which tries to calculate the two sums at once.Occultism
@KrazyKalle : Thanks. did the editPatricio
@KrazyKalle. Yes. What do you think I meant with the sentence between parentheses (when fixed ... immutable)?Occultism
By the way, you can write .filter(LedgerEntry::getCreditAmount) to use method references for the lambdas.Anesthetize
C
13

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.

Columbian answered 22/2, 2017 at 8:35 Comment(8)
Could new LedgerEntry() be replaced with LedgerEntry::new for the sake of readability?Tarahtaran
@CKing No. It's the initial value, not a method reference.Columbian
Noted. I assumed reduce would have an overloaded form that takes a functional interface such as a Supplier but guess there is no such thing.Tarahtaran
@RobbyCornelissen Thanks for the answer. However, I see one potential issue with this solution. It will throw a NullPointerException since new LedgerEntry() has the creditAmount and debitAmount set to null by default. If I make the entity (LedgerEntry) to have BigDecimal.ZERO as the default value it will fix the problem, but that doesn't look good on the entityPatricio
@CKing that's because this answer shows an invalid use of reduce(): the first parameter is the identity, and the BinaryOperator is not supposed to modify it. It should instead return another object. It is collection operations that require to modify their input (and indeed they take a Supplier as parameter).Correlation
@DidierL Pardon my ignorance. You are right. I'll look into updating my answer.Columbian
@Patricio does LedgerEntry have a constructor that takes values for creditAmount and debitAmount?Columbian
@RobbyCornelissen : It does not. As you have correctly pointed, pre java 8 version seems attractive.Patricio
W
1

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);
                    }));
Woosley answered 22/2, 2017 at 15:8 Comment(0)
H
0

if it's only two values, you can use Collectors method teeing() (JDK 12+)

From docs:

Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.

LedgerEntry ledgerEntry = ledgeredEntries.stream()
    .collect(Collectors.teeing(
        Collectors.reducing(BigDecimal.ZERO,
                            p -> p.getCreditAmount(), //you can replace with method reference
                            BigDecimal::add),
        Collectors.reducing(BigDecimal.ZERO,
                            p -> p.getDebitAmount(),
                            BigDecimal::add),
        LedgerEntry::new)
    );
Hooper answered 6/11, 2023 at 23:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.