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

15,243

Solution 1

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.

Solution 2

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());

Solution 3

You're trying to do two fundamentally contradicting things: filter out null elements (which is a local operation, based on a single element) and detect when all elements are null (which is a global operation, based on the entire list). Normally you should do these as two separate operations, that makes things a lot more readable.

Apart from the reduce() trick you've already found, you can also resort to underhand tricks, if you know that balance can never be negative for example, you can do something like

 long sum = account.stream().
                 mapToLong(a -> a.getCurrentBalance() == null ? 0 : a.getCurrentBalance()+1).
                 sum() - account.size();
 Long nullableSum = sum < 0 ? null : sum;

But you've got to ask yourself: is what you gain by only iterating across your collection once worth the cost of having written a piece of unreadable and fairly brittle code? In most cases the answer will be: no.

Share:
15,243

Related videos on Youtube

user384842
Author by

user384842

Updated on September 15, 2022

Comments

  • user384842
    user384842 over 1 year

    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

    • dieter
      dieter over 8 years
      sum() returns primitive integer. it can't be null.
    • user384842
      user384842 over 8 years
      That's the problem. What's the answer? ;-)
    • Paul Boddington
      Paul Boddington over 8 years
      It's best to just break it into 2 lines, one to filter out the nulls and another to return null if size is 0, sum otherwise. It is possible to do it in one line using reduce rather than filter but readability would suffer.
    • Maksym
      Maksym over 8 years
      Actually it's bad practice to return null at all (see NullObject Pattern)...
    • Paul Boddington
      Paul Boddington over 8 years
      No I meant turning it into an array or List to check the size.