Help AI coding agents use Java Streams and Collectors well in new code, review, and cleanup without replacing one antipattern with another.
100%
Does it follow best practices?
Impact
100%
2.77xAverage score across 3 eval scenarios
Passed
No findings from the security scan
Keep behavior identical (encounter order, exceptions, nulls, side effects, mutability) and stay within the project's Java baseline.
Write the requested file first, with the provided types where the prompt put them and nothing
the prompt did not ask for; a requested explanation goes into that file as a comment and into the
final message. A review (review.md when asked, also for rejections) gives the decision with one
safe snippet. Explain each change by the intent it expresses, the behavior it preserves, and the
cases the old code treated specially, such as a match terminal given no elements; never claim it
is faster.
| File | Purpose |
|---|---|
| hard-stops.md | Replacement antipatterns and the marker scan to run |
| stream-examples.md | Worked before/after examples from the reference set |
| java-stream-api.md | Java-version compatibility for stream and collector APIs |
Check the Java baseline first. java-stream-api.md lists minimum versions and fallbacks for older projects; do not emulate an unavailable API with stateful or multi-line lambdas.
Identify the requested result and pick the matching terminal or collector:
| Goal | Preferred API |
|---|---|
| Arbitrary match, all matches equivalent, order never picks the winner | filter(...).findAny() |
First match by encounter or sorted order, or code that took element 0 | filter(...).findFirst(); keep an existing sorted(...).filter(...).findFirst() that defines the winner |
| Existence check | anyMatch / noneMatch / allMatch |
| Exactly one match, duplicates must fail | filter(...).limit(2).toList(), then a switch on the size (below); no findFirst/findAny anywhere in that lookup |
| Two aggregates over same input | Collectors.teeing |
| Grouping/indexing | groupingBy, partitioningBy, or toMap with merge/null handling |
List<Printer> hits = printers.stream().filter(printer -> printer.hostname().equals(host)).limit(2).toList();
return switch (hits.size()) {
case 0 -> Optional.empty();
case 1 -> Optional.of(hits.get(0));
default -> throw new IllegalStateException("More than one printer at " + host);
};Checkpoint: before writing the chain, confirm the chosen terminal returns the requested type and that order, duplicates, and nulls in the input cannot change the winner.
Use intent-encoding terminals: anyMatch, count, joining, min/max, teeing, and
primitive terminals; let the stream produce the result instead of a forEach that fills a
container or counter. A loop that stops as soon as some element matches is an existence check
for anyMatch or noneMatch, with what the loop did on the match placed after it.
long overdue = orders.stream().filter(Order::isOverdue).count();Implementation: write the direct stream result; prefer
names.stream().map(String::toUpperCase).toList() over a manual ArrayList loop. Return
what the stream collects: no collection allocated first and filled afterwards, no list
collected only to be copied into the returned collection. When the result is only read,
collect to an unmodifiable one (toList(), Collectors.toUnmodifiableSet()), and
every path that produces nothing returns an empty unmodifiable collection.
External-mutation or performance review: show a sequential result-producing snippet first; for
million-item CPU maps, mention benchmarking a pure parallel variant and include:
"parallelStream() can be slower for small lists or call paths that are usually small."
For bounded blocking calls (Gatherers.mapConcurrent), carry each element with its result, call the provided service
directly, then filter/map/sort; no CompletableFuture fan-out.
List<Parcel> cleared = parcels.stream()
.gather(Gatherers.mapConcurrent(limit, parcel -> Map.entry(parcel, customsApi.clears(parcel))))
.filter(Map.Entry::getValue)
.map(Map.Entry::getKey)
.toList();Flatten nested sources deliberately.
flatMap, flatMap(Optional::stream), and mapMulti when clearer. When each element yields zero or one result, use mapMulti with a helper taking
(Event, Consumer<Alert>), as in events.<Alert>mapMulti(Monitor::alertIfCritical).mapToInt/mapToLong/mapToDouble.splitAsStream from a precompiled Pattern. Do not swap split("\\R") for String.lines(),
which recognizes fewer line terminators, unless the text is known to use only \\n and
\\r\\n.optionals.stream().flatMap(Optional::stream).collect(Collectors.toList());Choose accumulation/collectors by result semantics: use reduce(identity, op) for immutable
non-primitives, toMap with merge behavior and deliberate null-key/value handling, non-null keys
for groupingBy, partitioningBy for boolean splits, and flattened nested indexes when clearer.
Map<Boolean, List<Order>> byOverdue = orders.stream().collect(Collectors.partitioningBy(Order::isOverdue));
BigDecimal total = amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add);Carry element + result, never null sentinels.
final class SeatIndex {
// Latest booking per seat wins; seats without a code are skipped, as the loop did.
static Map<String, Booking> latestBySeat(List<Booking> bookings) {
return bookings.stream()
.filter(booking -> booking.seatCode() != null)
.collect(Collectors.toMap(Booking::seatCode, Function.identity(), SeatIndex::later, LinkedHashMap::new));
}
private static Booking later(Booking left, Booking right) {
return right.bookedAt().isAfter(left.bookedAt()) ? right : left;
}
}First read what the prompt allows. When it limits the change to one concern, say the way the
code is laid out or a single method, make only that change. Add no method, local, or import,
and leave every existing lambda as it is, even one a free cleanup would extract. Otherwise
keep stream lambdas as short glue or method references, and extract a named helper for a
lambda that would branch or wrap, a merge rule with tie, null, or ordering cases, or a nested
collector callback (a Stream<T> helper).
Keep imperative code when it is the clearer boundary for stateful output, checked IO,
mutation-heavy logic, or complex early exits. Writes to an external system (an API post, a
database or file write, a message send) are their own step. Gather what they need with a stream
chain, then make the writes in a plain loop or a terminal forEach that does nothing else;
never write from map or from a helper inside the chain. A loop that uses its index for
anything but element access stays a loop.
Verify changed branches for empty inputs, one element, duplicates, nulls, ordering, and baseline compatibility. Run the marker scan from hard-stops.md (no custom pool snippets unless asked), fix hits, and re-scan. In output, describe the code, never this workflow: no "skill", "scan", "marker", "hard stop", "checklist", "rubric", or "criteria" unless the user asked about the workflow itself.