ExerciseChallenge
Challenge
Predict the interleaving
20 minintermediate1–10 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Elements go through the whole pipeline one at a time, not stage by stage
- A stateful operation buffers and breaks that flow
- Short-circuiting works only because the terminal operation pulls
- sorted().findFirst() reads everything; min() does not
- Operation order is a performance decision, not a style one
Starter
Starter.java
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.stream.*;
/**
* Challenge: six pipelines. Predict every line of output before you run them.
*
* This is a prediction exercise, so the value is entirely in doing it in that
* order. If you run first you will find each result obvious and learn nothing.
*
* Write your predictions in the comment under each one, then run.
*/
public class Starter {
static final List<String> WORDS = List.of("delta", "echo", "alpha", "bravo", "foxtrot");
/** Traced operations, so you can see exactly what ran. */
static Predicate<String> filter(String label, Predicate<String> p) {
return s -> {
System.out.println(" " + label + " " + s);
return p.test(s);
};
}
static Function<String, String> map(String label, Function<String, String> f) {
return s -> {
System.out.println(" " + label + " " + s);
return f.apply(s);
};
}
public static void main(String[] args) {
// ── 1 ────────────────────────────────────────────────────────────
// PREDICT: how many lines, and in what order?
System.out.println("1: filter then map then forEach");
WORDS.stream()
.filter(filter("filter", s -> s.length() > 4))
.map(map("map", String::toUpperCase))
.forEach(s -> System.out.println(" out " + s));
// ── 2 ────────────────────────────────────────────────────────────
// PREDICT: does swapping the two operations change the OUTPUT, the
// amount of WORK, or both?
System.out.println("2: map then filter");
WORDS.stream()
.map(map("map", String::toUpperCase))
.filter(filter("filter", s -> s.length() > 4))
.forEach(s -> System.out.println(" out " + s));
// ── 3 ────────────────────────────────────────────────────────────
// PREDICT: how many filter lines before the first output?
System.out.println("3: findFirst");
WORDS.stream()
.filter(filter("filter", s -> s.startsWith("a")))
.findFirst()
.ifPresent(s -> System.out.println(" out " + s));
// ── 4 ────────────────────────────────────────────────────────────
// PREDICT: this one surprises people. How many map lines appear, and
// where do they appear relative to the sort?
System.out.println("4: sorted then findFirst");
WORDS.stream()
.map(map("map", String::toUpperCase))
.sorted()
.findFirst()
.ifPresent(s -> System.out.println(" out " + s));
// ── 5 ────────────────────────────────────────────────────────────
// PREDICT: how many elements does the source produce?
System.out.println("5: infinite source with limit");
AtomicInteger produced = new AtomicInteger();
Stream.iterate(1, n -> n * 2)
.peek(n -> produced.incrementAndGet())
.filter(n -> n > 10)
.limit(3)
.forEach(n -> System.out.println(" out " + n));
System.out.println(" source produced " + produced.get() + " elements");
// ── 6 ────────────────────────────────────────────────────────────
// PREDICT: does the audit see anything?
System.out.println("6: peek before count");
AtomicInteger peeked = new AtomicInteger();
long n = WORDS.stream().peek(s -> peeked.incrementAndGet()).count();
System.out.println(" count " + n + ", peek ran " + peeked.get() + " times");
// TODO 1: for each of the six, write what you predicted and what
// actually happened. Explain every difference.
// TODO 2: number 4 reads and maps the whole list to return one
// element. Say why findFirst cannot short-circuit through sorted, then
// rewrite it so it does not sort at all.
// TODO 3: number 2 does more work than number 1 for the same result.
// Quantify it — count the map lines in each — and state the rule.
// TODO 4: number 5 terminates over an infinite stream. Say which two
// properties make that possible, and what happens if you move limit
// before filter.
// TODO 5: number 6 is the one that changes with the JDK version. Say
// what Java 9 changed and what it means for side effects in peek.
}
}Run it locally:
cd exercises/java/java8/stream-lazy-evaluation/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Write the expected output line by line before running anything. Getting it wrong is the point of the exercise.
Hint 2
Which operations cannot emit their first element until they have seen the last one?
Hint 3
For the short-circuit cases, count how many elements the source produced rather than how many came out.
Hint 4
One of the six reads the whole source even though it returns one element. Find it and write the version that does not.
Done when
- You wrote predictions for all six before running them
- Every difference between prediction and output has a written explanation
- You can name the two operations that buffer, and why they must
- You rewrote the wasteful pipeline and measured the difference in elements read
← Back to Why does a stream with no terminal operation do nothing?