Java lambda to return null if empty list otherwise sum of values?
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.
Related videos on Youtube
user384842
Updated on September 15, 2022Comments
-
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 over 8 yearssum() returns primitive integer. it can't be null.
-
user384842 over 8 yearsThat's the problem. What's the answer? ;-)
-
Paul Boddington over 8 yearsIt's best to just break it into 2 lines, one to filter out the
null
s and another to returnnull
if size is0
, sum otherwise. It is possible to do it in one line usingreduce
rather thanfilter
but readability would suffer. -
Maksym over 8 yearsActually it's bad practice to return null at all (see NullObject Pattern)...
-
Paul Boddington over 8 yearsNo I meant turning it into an array or List to check the size.
-