From source to result
Start by identifying the source, the operation on each element and the requested result. For a list containing "ap", "long" and "cat", filtering lengths of at least three leaves two words. Applying String::length transforms them into 4 and 3. The terminal toList operation materializes those results. None of these choices requests alphabetical sorting or duplicate removal.
A lambda such as s -> s.length() >= 3 describes a function; it is not an immediate call for every element. A pipeline without a terminal operation has not yet computed the result. Even with a terminal operation, necessary work depends on the requested result.
During review, write the type after each stage: Stream<String> after filtering and Stream<Integer> after mapping. This avoids confusing map, which transforms each result, with filter, which decides whether an element proceeds. For an ordered source, also trace encounter order; it does not imply sorting that was never requested.
Capture and changing state
A lambda may use its own parameters and contextual values. A captured local variable must be final or effectively final and definitely assigned. With int limit = 3, creating a Predicate that reads limit and then assigning limit = 5 violates that rule. The captured limit does not update automatically.
Decide whether computation should use stable configuration or consult current state. For a stable decision, prepare the value before creating the function and do not reassign it. Introducing a one-element array merely to bypass the restriction may permit mutation, but changes the design: the function now depends on shared state. Compiler acceptance of that reference does not prove the decision remains consistent.
In pipelines, keep predicates and transformations free from source changes and dependencies that change during computation. A diagnostic question should separate three layers: code accepted by the compiler, API contract rules and domain-required behavior. Solving only the first does not establish the others.
Absence and fallback computation
Optional represents a present value or absence. If an integration returns null for an unknown label, ofNullable turns that result into explicit absence; of assumes a non-null value. Domain policy still needs deciding: absence may be acceptable, require a fallback or prevent the operation.
Do not confuse the returned value with work performed to obtain it. cached.orElse(load()) evaluates load before calling orElse even when cached already has a value. cached.orElseGet(() -> load()) supplies computation invoked only when needed. If load accesses a dependency or updates a counter, the difference affects cost and effects.
Avoid get without establishing presence. A map chain keeps the result optional and converts a null mapping result to empty; flatMap is for functions already returning Optional without adding another layer. That returned Optional must be non-null. These operations do not automatically classify service failure as missing data; that decision belongs to the integration contract.
Collect, flatten and define collisions
A Stream<Optional<String>> still contains containers, including empty ones. Flattening each Optional.stream contributes zero or one String to the result. This is not equivalent to calling get on every value, which can fail on absence. Types expose the distinction: map retains the layer returned by the function; flatMap integrates elements from returned streams.
Also choose the output collection contract. Stream.toList produces an unmodifiable list; Collectors.toList promises neither a concrete implementation nor mutability. If the consumer requires a mutable ArrayList, express that choice using Collectors.toCollection(ArrayList::new). This neither makes elements deeply immutable nor removes duplicates.
For maps, one key may represent several input lines. The toMap overload without a merge function rejects repeated keys. An additional function must reflect the domain: summing quantities, rejecting duplicates and choosing a version using a defined criterion are different policies. Choosing the last line merely to remove an exception can lose valid data.
A correct answer can omit effects
Counting four events does not establish that four audit records were written. In a stable-list pipeline with peek(audit::record).count(), the implementation may obtain the size without traversing elements. The count remains correct, but the effect placed in peek is unsuitable as an obligation to record each event. Making the pipeline sequential does not repair that dependency.
If the requirement is to perform an action on every element, model that responsibility explicitly, including failure observation and what follows partial effects. A loop may be clearer when order, each attempt’s state and recovery are part of the problem. No iteration form alone supplies a transaction over an external service.
A Stream is also not a reusable collection. After a terminal operation, create a new traversal from the source if supported. I/O-backed sources may require closing; their lifetime must then cover consumption. Materializing or counting results does not replace responsibility for managing the resource.
Empty input, order and grouping
In the dispatch service, “every order is approved” and “at least one order exists” are different conditions. allMatch returns true for empty input. That is consistent with the universal question but insufficient to authorize a batch required to contain work. On a stable list, combine a nonempty condition with checking every element. anyMatch answers a different question: whether at least one satisfies the predicate.
Encounter order also does not impose grouping in a parallel reduction. Subtraction yields different answers for (0-2)-3 and 0-(2-3). Using reduce with that operation violates associativity; changing only the initial value does not repair it. Identity and operation must support combining partial results.
Before parallelizing, write the mathematical rule and value limits. Integer addition may suit quantities when representation and limits are appropriate; averages, rounding and mutable state require their own analysis. “It ran faster” does not establish functional equivalence.
Guided review: stock totals
The snippet aggregates immutable lines by SKU. Inputs are stable, SKUs are non-null and every quantity contributes to its total. For Q with 3 and 6 units and R with 4, the expected result maps Q to 9 and R to 4; map entry iteration order is not part of the contract.
Integer::sum makes quantity merging explicit. The case assumes nonnegative values and totals within int; if that condition stops holding, revisit representation or detect excess before publication. Choosing the last value is not an alternative way to add.
The method returns a new map after collection completes. Its caller retains the active report until receiving a normal result and satisfying remaining checks. If computation fails, it must not present an empty map as a valid report. This separation does not promise atomic distributed publication. Assessment asks for snippet interpretation and answer selection; it requires neither code execution nor file production.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
final class StockReport {
record Line(String sku, int units) {}
// Stable inputs, non-null SKUs, nonnegative units and totals within int.
static Map<String, Integer> totals(List<Line> lines) {
return lines.stream().collect(Collectors.toMap(
Line::sku, Line::units, Integer::sum));
}
}
Lines Q=3, Q=6 and R=4 produce Q=9 and R=4. Input is unchanged; publication remains the caller’s responsibility.
Common pitfalls
Using peek for mandatory auditing; reusing a stream; accepting empty input with allMatch; computing fallback unnecessarily; subtracting in a parallel reduction.
Related topics: Collections, generics and keys · Exceptions, resources and files · Decimals, time and boundaries · Concurrency, state and cancellation
Pipeline shape does not prove effects or domain policy. Trace types, absence, collisions and combination requirements.
Reference: Java 25 API: Stream · Java SE 25; no preview features