Skip to content

martinfrancois/java-streams-skill

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

158 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Java Streams Skill for AI Agents

tessl

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.

Contents

Getting Started

1. Install

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

2. Use It

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.

Why This Exists

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:

  • check products in the user's favorite order;
  • run at most 8 stock checks at the same time;
  • keep only products that are in stock;
  • sort the final result by product name.

Without the skill, the generated code used two different approaches. Both looked reasonable at first, but both had important problems.

Version 1: 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.
  • In this example, the requested limit is per call to 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.
  • The concurrency limit is separate from the stream chain, which makes the code harder to reason about.

Version 2: Virtual Threads And A Semaphore

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:

  • It submits one task for every product before it starts collecting results. With a large list, that can create a large backlog of queued tasks even though only 8 checks run at once.
  • It uses 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.

With The Skill: Gatherers.mapConcurrent

When 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:

  • It keeps the limit of 8 checks inside the stream chain.
  • It uses bounded concurrency with backpressure: keep only a limited amount of work in flight, then start more work as earlier checks finish.
  • It keeps each product together with its stock-check result.
  • It filters and sorts in a clear order.

Common Stream Mistakes

The skill also helps with mistakes such as:

  • collecting filtered elements into a list, then checking isEmpty() or reading the first item;
  • using count() > 0 instead of anyMatch(...);
  • using sorted(...).findFirst() instead of min(...) or max(...);
  • mapping to a list and then calling String.join(...) instead of using Collectors.joining(...);
  • using boxed reduce(...) where a primitive stream terminal operation is clearer;
  • building nested sets or lists inside a map(...), then flattening afterward;
  • using toMap(...) without a merge function when duplicate keys are possible;
  • forgetting that natural sorting throws when null reaches the comparator;
  • making casual 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.

What It Helps With

Good fit:

  • replacing collect-then-inspect, count-for-existence, or sort-then-first patterns;
  • choosing findFirst() or findAny() without changing ordering semantics;
  • using flatMap for nested collections and Optional::stream for Stream<Optional<T>>;
  • using Collectors.joining, groupingBy, mapping, counting, summingInt / summingLong / summingDouble, summarizingInt / summarizingLong / summarizingDouble, partitioningBy, toMap, and teeing correctly;
  • selecting primitive streams and primitive stream terminal operations for primitive aggregation;
  • avoiding null-sensitive sorting and duplicate-key toMap failures;
  • deciding whether parallelStream() is actually appropriate;
  • choosing Java-version-compatible APIs such as takeWhile, mapMulti, Stream.toList(), and gatherers.

Poor fit:

  • broad Java style enforcement unrelated to streams or collectors;
  • replacing straightforward stateful loops with hard-to-read stream tricks;
  • large API redesigns or new dependencies without maintainer agreement;
  • changing business behavior just to make code look more functional.

How It's Evaluated

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.

Origin

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.

Contributing

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.

License

MIT. See LICENSE.