
Generated by Dall-e
Why Java Stream?
Since its introduction in Java 8, Stream has become more and more the preferred way of processing collection. But some of its APIs, are complicated and hard to understand, especially those of Collector and those for reduction.
I dare say that even experienced Java Stream users may still find them intimidating. Despite using it for several years and having prior experience with Stream-like operations in other programming languages, I still occasionally find some Stream-related tasks challenging.
Devising solutions in Stream operations does not come easy for those used to the ‘old’ ways of iterating through collections. That makes Stream a useful tool in designing interview questions.
Requirement
Given a list of integers, returns a list of positive numbers from the first elements until and excluding the first non-positive number in the original list
Example:
- Given
1, 2, 3, -1, 4, 5
, you should return1, 2, 3
- Given
0, 4, 3, -3
, you should return an empty list - Given an empty list, you should return an empty list
Extra: Implement takeWhile(positive)
method using other Stream operations
Discussion
takeWhile(Predicate)
(and its cousindropWhile(...)
) is a stateful intermediate method. The maintenance of state is necessary to implement such an operation
takeWhile
is essentially a filtering mechanism where elements are accepted so long as the predicate is still true
. Once the predicate results in false
, all subsequence elements are dropped from the stream. The implementation needs to maintain the correct filtering state, before and after the change of the predicate result event.
Imperative Solution
A simple imperative solution would look like this below
public List<Integer> takeWhilePositive(List<Integer> integers) {
List<Integer> result = new ArrayList<>();
for (int i : integers) {
if (i > 0) {
result.add(i);
} else break;
}
return result;
}
However, it takes advantage of implicit state management by loop control. Let’s look at how the next solution manages the state:
public List<Integer> takeWhilePositive(List<Integer> integers) {
List<Integer> result = new ArrayList<>();
boolean takeIn = true;
for (int i : integers) {
if (takeIn && i > 0) {
result.add(i);
} else takeIn = false;
}
return result;
}
The variable takeIn
is true
from the beginning until the very first non-positive element and remain false
until the end. Once the change happens, the resulting list result
stops collecting elements.
Stream-based Solutions
A Stream-based solution can be derived based on the above loop-based codes.
Stream’s forEach
provides an obvious alternative to for (Integer i : ints)
.
Due to stream’s requirement of referenced local variable being final
or effective final, the boolean takeIn = true
must be replaced by an object whose mutable state represents the binary boolean values. A simple workaround is to declare aMap<String, Boolean> takeInFlag = new HashMap<>()
in which the value of the"takekIn”
key tells us iftakeIn
is true.
public List<Integer> takeWhilePositive(List<Integer> integers) {
List<Integer> result = new ArrayList<>();
Map<String, Boolean> takeInFlag = new HashMap<>();
takeInFlag.put("takeIn", true);
integers.stream().forEach(i -> {
if (takeInFlag.get("takeIn") && i > 0) {
result.add(i);
} else takeInFlag.put("takeIn", false);
});
return result;
}
We should make use of other Stream operations like **filter(...)**
and **collect(...)**
however.
The next solution uses filter
to keep track of the state and to enforce the filtering logic
public List<Integer> takeWhilePositive(List<Integer> integers) {
List<Integer> result = new ArrayList<>();
Map<String, Boolean> takeInFlag = new HashMap<>();
takeInFlag.put("takeIn", true);
return integers.stream()
.filter(i -> {
if (takeInFlag.get("takeIn") && i > 0) {
return true;
} else {
takeInFlag.put("takeIn", false);
return false;
}
})
.collect(Collectors.toList());
}
Another alternative is having another method (e.g. map
) mutating the state followed by filter
enforcing it. Since map(...)
has to return something, we can return null
when state changes and having a .filter(Objects::nonNull)
afterward to drop all ineligible null
elements
public List<Integer> takeWhilePositive(List<Integer> integers) {
List<Integer> result = new ArrayList<>();
Map<String, Boolean> takeInFlag = new HashMap<>();
takeInFlag.put("takeIn", true);
return integers.stream()
.map( i -> {
if (takeInFlag.get("takeIn") && i > 0) {
return i;
} else {
takeInFlag.put("takeIn", false);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
Non-boolean Approach
A non-boolean-state alternative involves 2 steps:
- Identify the exact first point where state changes
- Extract all the elements leading (but excluding) to that point
A fixed list can afford us such a look-ahead. Here is a loop-based solution:
public List<Integer> takeWhilePositive-loopSublist(List<Integer> integers) {
int firstNonPositiveIdx = -1;
for (int i = 0; i < integers.size(); i++) {
if (integers.get(i) <= 0) {
firstNonPositiveIdx = i;
break;
}
}
if (firstNonPositiveIdx < 0) {
return integers;
}
return integers.subList(0, firstNonPositiveIdx);
}
and its Stream-based cousins
public List<Integer> takeWhilePositive-streamLimit(List<Integer> integers) {
var firstNonPositiveIdx = IntStream.range(0, integers.size())
.filter(idx -> integers.get(idx) <= 0)
.findFirst();
if (firstNonPositiveIdx.isEmpty()) {
return integers;
}
return integers.stream()
.limit(firstNonPositiveIdx.getAsInt())
.collect(Collectors.toList());
}
public List<Integer> takeWhilePositive-streamSubList(List<Integer> integers) {
var firstNonPositiveIdx = IntStream.range(0, integers.size())
.filter(idx -> integers.get(idx) <= 0)
.findFirst();
if (firstNonPositiveIdx.isEmpty()) {
return integers;
}
return integers.subList(0, firstNonPositiveIdx.getAsInt());
}