Java Stream Interview Tests - takeWhile

Stream has been an important part of Java development since Java 8. This test assesses candidates' understanding of Stream’s mechanics

Gerald Nguyen
Gerald Nguyen
4 min read ·
Previous | Next
Also on Medium
On this page
Generated by Dall-e

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:

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 = truemust 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:

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

Sample Codes

Checkout sample solutions and tests in my GitHub