Q16 of 40 · Core Java
What is the difference between intermediate and terminal stream operations?
Short answer
Short answer: Intermediate operations (filter, map, sorted, flatMap) are lazy — they return a new Stream and don't execute until a terminal operation is called. Terminal operations (collect, forEach, reduce, count, findFirst) trigger the pipeline, consume the stream, and cannot be called again on the same stream.
Detail
Intermediate operations transform a Stream into another Stream. They are lazy: no element is processed until a terminal operation forces evaluation. This enables short-circuit optimisation — findFirst() after a filter() stops processing the moment it finds the first match, even if the source has a million elements.
Intermediate operations can be stateless (filter, map — each element processed independently) or stateful (sorted, distinct, limit — may need to see all elements before producing any output). Stateful operations on parallel streams require synchronisation and are potentially expensive.
Terminal operations consume the stream. After a terminal operation completes, the stream is exhausted — calling any operation on it again throws IllegalStateException. Terminal operations are either:
- Eager reducers:
collect(),reduce(),count(),toArray()— process all elements - Short-circuit:
findFirst(),findAny(),anyMatch(),allMatch(),noneMatch()— can stop early - Side-effect:
forEach(),forEachOrdered()— execute an action per element (avoid for transformations)
Practical implication: build and debug pipelines incrementally. If you want to inspect elements mid-pipeline, use .peek(Consumer) (an intermediate operation for debugging) without breaking the chain. In production code, remove or guard peek calls because they add overhead on large streams.
// EXAMPLE
IntermediateTerminal.java
List<String> tags = List.of("smoke", "regression", "smoke", "e2e", "regression");
// Intermediate ops: lazy, each returns Stream<T>
Stream<String> pipeline = tags.stream()
.filter(t -> !"smoke".equals(t)) // intermediate — lazy
.distinct() // intermediate — stateful, lazy
.sorted(); // intermediate — stateful, lazy
// Nothing has executed yet!
// Terminal op: triggers the pipeline
List<String> result = pipeline.collect(Collectors.toList());
// → ["e2e", "regression"]
// pipeline.count(); // ❌ IllegalStateException — stream already consumed
// Short-circuit: stops at first match, doesn't process remaining
boolean hasE2E = tags.stream()
.filter(t -> t.startsWith("e"))
.findFirst() // terminal, short-circuit
.isPresent();
// peek() for debugging — remove from production
List<String> debugResult = tags.stream()
.peek(t -> System.out.println("before filter: " + t)) // intermediate
.filter(t -> t.length() > 4)
.peek(t -> System.out.println("after filter: " + t)) // intermediate
.toList(); // terminal