Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java lambda to return null if empty list otherwise sum of values?

Tags:

java

lambda

If I want to total a list of accounts' current balances, I can do:

accountOverview.setCurrentBalance(account.stream().
                filter(a -> a.getCurrentBalance() != null).
                mapToLong(a -> a.getCurrentBalance()).
                sum());

But this expression will return 0, even if all the balances are null. I would like it to return null if all the balances are null, 0 if there are non-null 0 balances, and the sum of the balances otherwise.

How can I do this with a lambda expression?

Many thanks

like image 264
user384842 Avatar asked Aug 03 '15 10:08

user384842


2 Answers

Once you filtered them from the stream, there's no way to know if all the balances were null (unless check what count() returns but then you won't be able to use the stream since it's a terminal operation).

Doing two passes over the data is probably the straight-forward solution, and I would probably go with that first:

boolean allNulls = account.stream().map(Account::getBalance).allMatch(Objects::isNull);

Long sum = allNulls ? null : account.stream().map(Account::getBalance).filter(Objects::nonNull).mapToLong(l -> l).sum();

You could get rid of the filtering step with your solution with reduce, although the readability maybe not be the best:

Long sum = account.stream()
                  .reduce(null, (l1, l2) -> l1 == null ? l2 :
                                                         l2 == null ? l1 : Long.valueOf(l1 + l2));

Notice the Long.valueOf call. It's to avoid that the type of the conditional expression is long, and hence a NPE on some edge cases.


Another solution would be to use the Optional API. First, create a Stream<Optional<Long>> from the balances' values and reduce them:
Optional<Long> opt = account.stream()
                            .map(Account::getBalance)
                            .flatMap(l -> Stream.of(Optional.ofNullable(l)))
                            .reduce(Optional.empty(),
                                    (o1, o2) -> o1.isPresent() ? o1.map(l -> l + o2.orElse(0L)) : o2);

This will give you an Optional<Long> that will be empty if all the values were null, otherwise it'll give you the sum of the non-null values.

Or you might want to create a custom collector for this:

class SumIntoOptional {

    private boolean allNull = true;
    private long sum = 0L;

    public SumIntoOptional() {}

    public void add(Long value) {
        if(value != null) {
            allNull = false;
            sum += value;
        }
    }

    public void merge(SumIntoOptional other) {
        if(!other.allNull) {
            allNull = false;
            sum += other.sum;
        }
    }

    public OptionalLong getSum() {
        return allNull ? OptionalLong.empty() : OptionalLong.of(sum);
    }
}

and then:

OptionalLong opt = account.stream().map(Account::getBalance).collect(SumIntoOptional::new, SumIntoOptional::add, SumIntoOptional::merge).getSum();


As you can see, there are various ways to achieve this, so my advice would be: choose the most readable first. If performance problems arise with your solution, check if it could be improved (by either turning the stream in parallel or using another alternative). But measure, don't guess.
like image 105
Alexis C. Avatar answered Oct 14 '22 21:10

Alexis C.


For now, I'm going with this. Thoughts?

        accountOverview.setCurrentBalance(account.stream().
                filter(a -> a.getCurrentBalance() != null).
                map(a -> a.getCurrentBalance()).
                reduce(null, (i,j) -> { if (i == null) { return j; } else { return i+j; } }));

Because I've filtered nulls already, I'm guaranteed not to hit any. By making the initial param to reduce 'null', I can ensure that I get null back on an empty list.

Feels a bit hard/confusing to read though. Would like a nicer solution..

EDIT Thanks to pbabcdefp, I've gone with this rather more respectable solution:

        List<Account> filtered = account.stream().
                filter(a -> a.getCurrentBalance() != null).
                collect(Collectors.toList());

        accountOverview.setCurrentBalance(filtered.size() == 0?null:
            filtered.stream().mapToLong(a -> a.getCurrentBalance()).
            sum());
like image 24
user384842 Avatar answered Oct 14 '22 20:10

user384842