Java 8 Stream API Interview Questions, With the Output to Prove It
Stream interview questions sound like definitions, but the follow-up is always what the code prints. Run on JDK 8, 21 and 25, a filter over a million elements ran 7 times before findFirst, sorted() did no work at all on a TreeSet, a parallel reduce with identity 10 returned 50 instead of 20 in 1,000 runs out of 1,000, and the classic infinite-stream question did not hang: it returned -2147483648 after about seven seconds.
Java 8 stream interview questions sound like vocabulary checks: intermediate or terminal, map or flatMap, findFirst or findAny. The follow-up is nearly always "so what does this print?", and a few of the stock answers are wrong, or were only right on Java 8.
Every output below came from running the program in this session on three JDKs: Temurin 1.8.0_502, Temurin 21.0.12 and OpenJDK 25.0.1, on a machine reporting 8 cores. Where the three agree, one output is shown; where they disagree, that difference is the answer. If you want the everyday operations with examples (grouping, toMap, summing), Java Stream API examples covers those, and its gotchas are not repeated here. To switch between JDKs the way these runs did, setting JAVA_HOME is the whole trick.
In what order does a pipeline actually run?
The textbook answer is that intermediate operations are lazy and the terminal operation runs them. What the question is really about is the order:
Stream.of("b", "c", "a")
.filter(s -> { System.out.println("filter " + s); return true; })
.map(s -> { System.out.println("map " + s); return s.toUpperCase(); })
.forEach(s -> System.out.println("forEach " + s));
filter b
map b
forEach B
filter c
map c
forEach C
filter a
map a
forEach A
The filters do not all run first. Each element goes through the whole pipeline before the next one starts, and that is what lets a pipeline stop early. Over IntStream.rangeClosed(1, 1_000_000) with a counting predicate:
findFirst -> 7, filter ran 7 times
limit(3) -> [7, 14, 21], filter ran 21 times
anyMatch -> true, predicate ran 6 times
The source has a million elements and findFirst called the predicate seven times. Laziness here means per element, not "later".
Is sorted() lazy?
It is an intermediate operation, and a stateful one. The package documentation gives the reason: "one cannot produce any results from sorting a stream until one has seen all elements of the stream." Put sorted() between the filter and the map above and the print order changes:
filter b
filter c
filter a
map a
forEach A
map b
forEach B
map c
forEach C
Every filter runs before the first map. Over a million elements, list.stream().filter(...).sorted().findFirst() ran the filter 1,000,000 times on all three JDKs.
Then the result that goes against the stock answer. The same pipeline over a TreeSet holding the same million integers:
ArrayList.stream().filter.sorted.findFirst -> 7, filter ran 1,000,000 times
TreeSet.stream().filter.sorted.findFirst -> 7, filter ran 7 times
A TreeSet stream already knows it is sorted, and SortedOps in the JDK source checks for exactly that: "If the input is already naturally sorted and this operation also naturally sorted then this is a no-op". Only natural order gets the shortcut. With sorted(Comparator.reverseOrder()) the filter ran 1,000,000 times, though the map after the sort still ran only 3 times for a limit(3).
And a Java 8 difference, over IntStream.rangeClosed(1, 1_000_000).boxed():
// JDK 8
IntStream.boxed.filter.sorted.findFirst -> 7, filter ran 1,000,000 times
// JDK 21 and JDK 25
IntStream.boxed.filter.sorted.findFirst -> 7, filter ran 7 times
In the JDK 8 source boxed() is mapToObj(Integer::valueOf), and a map drops the sorted flag. In the JDK 21 source it is mapToObj(Integer::valueOf, 0), which keeps it. Same code, a million predicate calls apart.
map or flatMap?
map turns each element into exactly one result. flatMap turns each element into a stream and joins those streams into one. Splitting two lines into words:
List<String> lines = Arrays.asList("to be or", "not to be");
lines.stream().map(l -> l.split(" "))... // 2 elements
lines.stream().flatMap(l -> Arrays.stream(l.split(" ")))... // 6 elements
map size=2 first element=String[] 3 items
flatMap size=6 [to, be, or, not, to, be]
flatMap distinct [to, be, or, not]
With map you get a stream of two arrays; only flatMap gives a stream of words that distinct() can work on. The follow-up interviewers like is about primitive arrays:
Stream.of(int[]).count() = 1
Arrays.stream(int[]).count() = 3
Arrays.asList(int[]).size() = 1
Stream.of(Integer[]).count() = 3
Stream.of(T...) cannot make T an int, so the whole int[] becomes a single element, while Arrays.stream(int[]) streams the three values. All three JDKs printed the same four lines.
What happens with an infinite stream?
Stream.iterate and Stream.generate never end on their own, and limit makes them finite:
iterate.limit(10) [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
iterate.limit(33)last 0
The 33rd element should be 2³², and it is 0, because int overflow wraps and a stream does nothing to stop it. That matters for the classic question:
Stream.iterate(1, i -> i + 1).filter(i -> i < 10).limit(10).collect(Collectors.toList());
The expected answer is "it hangs forever, because only nine elements ever pass the filter". It did not:
iterate.filter(<10).limit(10) [1, 2, 3, 4, 5, 6, 7, 8, 9, -2147483648] in 6843 ms
The counter went past Integer.MAX_VALUE, wrapped to -2147483648, which is less than 10, and the tenth element arrived. The time is the median of three runs on JDK 25 (6,741, 6,843 and 7,242 ms, on a machine shared with other work); single runs on JDK 8 and 21 took 12,039 and 8,036 ms and returned the same list. A wrong list after a few seconds is harder to spot than a hang.
Java 9 added the proper tools, and both stop at the condition:
iterate(seed, hasNext, next) [1, 2, 3, 4, 5, 6, 7, 8, 9]
iterate.takeWhile(<10) [1, 2, 3, 4, 5, 6, 7, 8, 9]
On JDK 8 neither compiles (error: cannot find symbol ... symbol: method takeWhile((i)->i < 10)). There, a bounded source such as IntStream.range(1, 10) printed the same nine numbers.
Why does reduce give a different answer in parallel?
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
nums.stream().reduce(10, Integer::sum);
nums.parallelStream().reduce(10, Integer::sum); // repeated 1,000 times
sequential reduce(10, sum) = 20
parallel reduce(10, sum) over 1000 runs = {50=1000}
sequential reduce(0, a-b) = -10
parallel reduce(0, a-b) over 1000 runs = {0=1000}
The usual belief is that parallel bugs are random. This one returned 50 in 1,000 runs out of 1,000, on each of the three JDKs. Fifty is 1+2+3+4 plus the identity four times, because the identity is used again for every chunk. The Stream javadoc states the rule: "The identity value must be an identity for the accumulator function. This means that for all t, accumulator.apply(identity, t) is equal to t." 10 is not an identity for addition, and subtraction is not associative, so both break.
A wrong answer that never changes will pass every test you run twice. Reduce with 0 and add the offset afterwards; mapToInt(...).sum() + 10 printed 20.
Can a parallel stream add to an ArrayList?
It compiles, and it sometimes works. IntStream.range(0, 100_000).parallel().forEach(out::add), ten runs on JDK 25:
run 1 forEach(list::add): size 30975 (lost 69025, nulls 139)
run 2 forEach(list::add): ArrayIndexOutOfBoundsException: null
run 5 forEach(list::add): size 20730 (lost 79270, nulls 0)
run 10 forEach(list::add): size 100000 (lost 0, nulls 0)
Across thirty runs, ten per JDK, the loop threw ArrayIndexOutOfBoundsException 5 times, kept all 100,000 elements only twice, and in the rest lost between 6,693 and 82,937 of them. Several lists also held nulls that nothing had added. Collectors.toList() on the same parallel stream returned 100,000 elements in order in all thirty runs.
The package documentation uses this exact example: "If executed in parallel, the non-thread-safety of ArrayList would cause incorrect results". The good answer: the side effect in the lambda is the bug, and collect is the fix, because the stream library manages the container instead of your lambda writing to a shared one. ArrayList against LinkedList explains the backing-array copy on growth, which is what the threads are racing on.
findFirst or findAny?
On a sequential stream they returned the same element in every run. On a parallel one, searching 1 to 1,000 for multiples of 100:
parallel findAny over 1000 runs: {100=6, 200=17, 300=161, 400=1, 500=7, 600=15, 700=737, 900=55, 1000=1}
parallel findFirst over 1000 runs: {100=1000}
sequential findAny over 1000 runs: {100=1000}
That was JDK 25. JDK 8 returned 700 in 748 runs and JDK 21 in 765. So findAny is not a random pick either: the result is heavily skewed, and the shape reflects how the work was split rather than chance. The javadoc promises only that it "is free to select any element in the stream", and points to findFirst when you need a stable result.
The same split shows up with forEach:
parallel forEach : 7 6 3 2 5 1 9 4 8 10
parallel forEachOrdered : 1 2 3 4 5 6 7 8 9 10
Which answers changed since Java 8?
The query says Java 8, and plenty of interview answers still assume it. Three of them no longer hold.
peek with count(). Arrays.asList(1, 2, 3).stream().peek(print).count():
// JDK 8
peek 1
peek 2
peek 3
count = 3
// JDK 21 and JDK 25
count = 3
Newer JDKs work out the count from the list's size and skip the pipeline, and the Java 21 count() javadoc says they may: "In such cases no source elements will be traversed and no intermediate operations will be evaluated." The Java 8 Stream javadoc has no such note. With a filter before the peek, all three JDKs printed every element, because the size is no longer known.
boxed().sorted(), shown above: a million predicate calls on 8, seven on 21 and 25.
Stream.toList(), added in Java 16:
Collectors.toList().add -> ok
Stream.toList().add -> java.lang.UnsupportedOperationException: null
[a, null] Stream.toList() with null -> ok
Collectors.toUnmodifiableList() with null -> java.lang.NullPointerException: null
Collectors.toList() class: java.util.ArrayList
Stream.toList() class: java.util.ImmutableCollections$ListN
The javadoc: "The returned List is unmodifiable; calls to any mutator method will always cause UnsupportedOperationException to be thrown." Replacing collect(Collectors.toList()) with toList() looks like tidying up, and it breaks any caller that adds to the list. On JDK 8 the line does not compile.
Answering stream questions out loud
Each question has a definition and a consequence, and the interviewer is waiting for the consequence. Laziness is per element, so findFirst touched seven of a million. sorted() is stateful, so it reads everything, unless the source is already sorted. The identity is used per chunk, so reduce(10, ...) returns 50. If they ask about parallel streams in general, processes against threads is the same trade-off in another language.
[!TAKEAWAY] Run the pipeline before you answer. Seven predicate calls out of a million, a
sorted()that did nothing on aTreeSet,50from a parallel sum of1..4, and an "infinite" stream that returned-2147483648are hard to recite convincingly and hard to forget once you have seen them.
Frequently asked questions
- Are intermediate operations executed one stage at a time?
- No. Each element travels through the whole pipeline before the next element starts, which is visible in the print order: filter b, map b, forEach B, then filter c. A stateful operation such as sorted() is the exception, because it has to see every element before it can pass any on.
- Does sorted() always process the whole stream?
- Only when the stream does not already know it is sorted. Over an ArrayList a filter before sorted().findFirst() ran 1,000,000 times; over a TreeSet with the same elements it ran 7 times, because the JDK skips a natural-order sort on a stream flagged as sorted. A reverse-order comparator removes that shortcut.
- Is findAny random on a parallel stream?
- Not in any useful sense. Over 1,000 runs it returned 700 about three times in four and 100 fewer than one time in a hundred, on all three JDKs. The javadoc only promises that it may return any matching element, so treat the result as unspecified rather than as a random sample.
- Why is a parallel reduce wrong when the sequential one is right?
- Because the identity is applied once per chunk, not once per stream. reduce(10, Integer::sum) over 1, 2, 3, 4 returned 20 sequentially and 50 in parallel: the sum plus 10 four times. Use a true identity such as 0 and add the offset afterwards, and only reduce with an associative function.
- Is Stream.toList() the same as collect(Collectors.toList())?
- No. Stream.toList() arrived in Java 16 and returns an unmodifiable list, so add() threw UnsupportedOperationException, while the collector's list accepted it. toList() still allows null elements, whereas Collectors.toUnmodifiableList() threw NullPointerException on one. On Java 8 toList() does not compile.