Help AI coding agents use Java Streams and Collectors well in new code, review, and cleanup without replacing one antipattern with another.
100
100%
Does it follow best practices?
Impact
100%
2.17xAverage score across 4 eval scenarios
Passed
No known issues
AI agents often know Java streams well enough to chain filter, map, and collect, but not
enough to choose the right stream operation for the job in new code, reviews, and cleanup.
They write code that looks modern at first glance, then materializes a list just to check whether
anything matched, sorts a whole stream to get one newest item, counts for existence, uses boxed
numeric reductions, changes findFirst() to findAny() without noticing the order contract, or
adds parallelStream() where it makes the code slower or less predictable.
This skill gives the agent a compact decision guide before it writes or changes stream code: choose the stream terminal operation that matches the result, preserve ordering and null behavior, pick collectors by map semantics, use primitive streams for primitive totals, and treat parallel streams as a design choice rather than a default optimization.
It also tells the agent to check the project Java version first. The right stream code for Java 8 may be different from the right code for Java 17, Java 21, or Java 24.
Install the published Tessl plugin using the option that fits your setup:
| Tool | Command |
|---|---|
| npm | npx tessl i martinfrancois/java-streams |
| yarn | yarn dlx tessl i martinfrancois/java-streams |
| pnpm | pnpx tessl i martinfrancois/java-streams |
| bun | bunx tessl i martinfrancois/java-streams |
| Tessl CLI | tessl i martinfrancois/java-streams |
Agents that support skill auto-selection, such as
Codex and
Claude Code, can choose this skill automatically from the
task or code context. The task does not need to say stream by name.
It can trigger when Java code uses streams, collectors, primitive streams, findFirst() /
findAny(), match terminal operations, flatMap, mapMulti, joining, min / max, sum,
groupingBy, toMap, partitioningBy, teeing, takeWhile / dropWhile, or parallel stream
behavior.
For important stream-heavy work, you can still name the skill explicitly:
Use $java-streams to implement this Java feature with stream and collector best practices.For cleanup work:
Use $java-streams to clean up this Java stream chain without changing behavior.For reviews:
Use $java-streams to review this Java stream code and suggest any fixes.AI can write Java code that looks modern because it uses streams. If you do not know streams very
well, that code can look plausible in review. But looking plausible is not enough: the code can
still use the wrong kind of concurrency, build lists it does not need, use null as a hidden signal,
fail when keys are duplicated, or choose a terminal operation that does not match the real intent.
This skill helps the agent write stream code that is easier to read, safer to change, and often more
efficient. It pushes the agent toward the stream operation that matches the job, such as anyMatch
instead of collecting a list just to check if a match exists. It also helps the agent review existing
stream code and explain what should be fixed.
For example, imagine code that checks a user's favorite products against a remote inventory API. That API call blocks while it waits for the remote service. The code should:
Without the skill, the generated code used two different approaches. Both looked reasonable at first, but both had important problems.
parallelStream()private static final Semaphore STOCK_CHECKS = new Semaphore(8);
List<Product> favoriteProducts(User user) {
return user.favoriteProducts().parallelStream()
.filter(product -> {
try {
STOCK_CHECKS.acquire();
return InventoryApi.check(product.sku());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
STOCK_CHECKS.release();
}
})
.sorted(Comparator.comparing(Product::name))
.toList();
}This keeps the basic filter and sort behavior, but it is still not a good solution:
parallelStream() uses Java's common fork-join pool. The
ForkJoinPool Javadoc
says the pool can adjust its worker threads in some cases, but those adjustments are not
guaranteed for blocked I/O. That makes it a poor default for blocking remote API calls.favoriteProducts(user), but the static
semaphore is shared by every call to the method. Two users calling it at the same time can block
each other instead of each call getting its own limit of up to 8 stock checks.List<Product> favoriteProducts(User user) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var permits = new Semaphore(8);
return user.favoriteProducts().stream()
.map(product -> executor.submit(() -> {
permits.acquire();
try {
return InventoryApi.check(product.sku()) ? product : null;
} finally {
permits.release();
}
}))
.map(FavoriteProducts::getUnchecked)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(Product::name))
.toList();
}
}
private static Product getUnchecked(Future<Product> future) {
try {
return future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
}
}This avoids parallelStream(), and it limits how many checks are active at once. But it still has
problems:
null as a hidden signal for "not in stock." A reader has to remember that special
meaning until the later filter(Objects::nonNull). If someone changes the stream chain before
that filter, the code can easily break.Gatherers.mapConcurrentWhen the project uses Java 24 or newer, Gatherers.mapConcurrent gives a bounded concurrent stream
operation for this kind of blocking per-element work:
List<Product> favoriteProducts(User user) {
return user.favoriteProducts().stream()
.gather(Gatherers.mapConcurrent(8,
product -> Map.entry(product, InventoryApi.check(product.sku()))))
.filter(Map.Entry::getValue)
.map(Map.Entry::getKey)
.sorted(Comparator.comparing(Product::name))
.toList();
}This version is clearer:
The skill also helps with mistakes such as:
isEmpty() or reading the first item;count() > 0 instead of anyMatch(...);sorted(...).findFirst() instead of min(...) or max(...);String.join(...) instead of using Collectors.joining(...);reduce(...) where a primitive stream terminal operation is clearer;map(...), then flattening afterward;toMap(...) without a merge function when duplicate keys are possible;null reaches the comparator;parallelStream() changes without checking data size, CPU cost, shared state,
ordering, or blocking IO.The goal is not to turn every loop into a stream. The goal is to use streams and collectors when they make the code clearer, safer, or easier to check.
Good fit:
findFirst() or findAny() without changing ordering semantics;flatMap for nested collections and Optional::stream for Stream<Optional<T>>;Collectors.joining, groupingBy, mapping, counting, summingInt /
summingLong / summingDouble, summarizingInt / summarizingLong /
summarizingDouble, partitioningBy, toMap, and teeing correctly;toMap failures;parallelStream() is actually appropriate;takeWhile, mapMulti, Stream.toList(), and
gatherers.Poor fit:
The skill is tested on Java stream implementation, review, and cleanup tasks. Each task is run without the skill and with the skill, then scored on whether the agent keeps the requested behavior while choosing better stream and collector code.
The evals cover common places where agents write plausible-looking but weak Java: collecting before
checking existence, using the wrong terminal operation, losing encounter order, mishandling
duplicate keys or nulls, overusing parallelStream(), and missing Java-version-specific APIs such
as takeWhile, teeing, mapMulti, Stream.toList(), and gatherers.
Current published scores are shown on the Tessl plugin.
The stream examples and pattern catalog are based on the code examples from François Martin's conference talk "I didn't know you could do that with Java Streams", with the public example source here: https://github.com/martinfrancois/jfokus-2026/blob/main/code.md.
See CONTRIBUTING.md for local validation, eval design rules, commit-message format, and release workflow details.
AI-assisted contributions are welcome when they are transparent, reviewed, and owned by a human. See AI_CONTRIBUTION_POLICY.md.
For suspected vulnerabilities, use the private reporting path in SECURITY.md.
MIT. See LICENSE.