Skip to content
Back to skills

Java Streams

ASecurity

Stream pipelines as a design decision: when a stream is clearer than a loop and when it is not, side-effect-free stages and mutable reduction with collectors, the toMap and groupingBy traps, Collection versus Stream as a return type, streams that hold an open resource, parallel streams and the shared common pool, and Gatherers for custom intermediate operations. Use when a pipeline mutates state outside itself or uses forEach to accumulate, when Collectors.toMap throws IllegalStateException o...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
developmentjavaexpressapiperformance

Works with

  • cursor
  • terminal
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add robsonkades/agent-skills --skill java-streams --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Java Streams?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Java Streams
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/robsonkades-java-streams/badge)](https://www.skillsdirectory.com/skills/robsonkades-java-streams)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: java-streams
description: >
  Stream pipelines as a design decision: when a stream is clearer than a loop and when it is
  not, side-effect-free stages and mutable reduction with collectors, the toMap and
  groupingBy traps, Collection versus Stream as a return type, streams that hold an open
  resource, parallel streams and the shared common pool, and Gatherers for custom
  intermediate operations. Use when a pipeline mutates state outside itself or uses forEach
  to accumulate, when Collectors.toMap throws IllegalStateException or NullPointerException,
  when a method returns a Stream that callers iterate twice, when a stream over Files.lines
  or a JDBC cursor is never closed, when parallelStream() appears — especially with blocking
  I/O — or when a loop is being rewritten as a stream for its own sake. Does not cover
  lambda capture and functional interfaces (java-lambdas-and-functional-interfaces),
  ForkJoinPool internals (forkjoinpool-and-work-stealing), or collection choice and
  complexity.
---

# Java Streams

## Purpose

Use streams where they express a transformation better than a loop, and keep them honest:
non-interfering callbacks, state owned by the reduction or transformation, and explicit resource
and execution ownership. Two failure modes: required effects hidden in elidable intermediate
callbacks or unsafe parallel mutation; and `parallelStream()` applied to blocking work,
where requests within each JVM can contend on its shared `ForkJoinPool.commonPool` whose
effective parallelism depends on runtime configuration
and the processors visible to the JVM.

## Workflow

Inspect the project JDK, source ownership and finiteness, null/order/mutability contract and workload
before recommending a rewrite. A review produces findings; change code when the request authorizes
it. Clarify unknown source bounds or result semantics only when they change the decision.
Core examples target Java 21; Gatherers require Java 24+, and structured-concurrency
alternatives need their exact preview policy checked. Do not upgrade or enable preview for a
pipeline cleanup. Examples are partial snippets with application types and imports omitted.

1. **Ask what the code is doing.** Transform-filter-aggregate over a collection → consider a stream.
   Loop with early exit on complex conditions, index arithmetic, two collections in lockstep,
   mutation of local state, or a checked exception per element → loop.
2. **Keep required effects out of elidable callbacks.** Use non-interfering, stateless
   behavioral parameters; supported stateful operations and gatherers own their internal state.
   Choose an explicit terminal action or loop when effects are part of the result contract.
3. **Pick the collector deliberately**, not the first one that compiles: `toList` when order
   matters, `toMap` with deliberate duplicate rejection or merge policy, `groupingBy` with an explicit downstream,
   `teeing` when two aggregates are needed in one pass.
4. **Decide the return type at the API boundary.** Prefer a `Collection` for materialized
   results needing repeated access. A `Stream` can express lazy traversal or an existing
   supported API; preserve compatibility and document whether the caller must close it.
5. **Only consider parallel with a measurement.** Blocking work needs explicit concurrency,
   cancellation and executor ownership; default parallel streams commonly use the shared
   common pool.
6. **Trace and verify the contract:** source, what each stage computes, terminal result/effects,
   termination and cleanup. A single-use stream can buffer or make multiple processing passes internally;
   reuse adequate checks and investigate only the remaining material gaps.

## Rules

- A stream is not a better loop; it is a different expression of one. Prefer a stream when the
  pipeline reads as a description of the result. Prefer a loop when the code needs an early
  `return` mid-iteration, `break` with several conditions, index or neighbour access, mutation
  of local variables, or a `try`/`catch` per element.
- Require non-interference and statelessness for behavioral parameters. A `map` or `filter` that adds to an external
  list, increments a counter, writes a log per element, or calls a mutating service is not a
  reliable place for required effects: even an explicitly sequential pipeline may elide a stage
  or short-circuit. Put required effects in an explicit loop or suitable terminal action.
- `forEach` is a terminal action, including output or mutation under an explicit ownership
  contract. A confined sequential `forEach(list::add)` can be correct; a lawful collector often
  expresses the result better and supports isolated parallel accumulation. Preserve callers'
  null/mutability contract rather than mechanically replacing it with `toList()`.
- `Collectors.toMap` without a merge function deliberately rejects duplicate keys; use it when
  uniqueness is an invariant and test the failure. Supply a keep/merge policy only when duplicates
  are valid. A merge function returning null removes the mapping; it does not simply skip the
  incoming value. Current JDK implementations reject null mapped values;
  do not depend on implementation-specific null tolerance—normalize, use a suitable custom
  collector/map, or write an explicit loop.
- Give `groupingBy` an explicit downstream collector whenever the group is not a plain list —
  `counting()`, `summingLong(...)`, `mapping(..., toList())`, `reducing(...)`. Deep nesting is a
  readability/shape signal; a record key or explicit result model may be clearer, without a fixed threshold.
- `reduce` is for associative, side-effect-free combination or selection of a result. Anything
  that accumulates into a mutable container is `collect`. Mutating a reduction's identity can
  appear to work sequentially but violates the contract and can corrupt parallel results.
- Prefer a `Collection` for a new API exposing already materialised results for repeated access. A
  stream is single-use—a second terminal traversal is invalid—has no collection-style size/index
  API even though its spliterator may know an exact size. Return a `Stream` when the result is lazily produced, is
  large enough that materialising it is a real cost, or is backed by a resource. Preserve an
  adequate existing `Stream` contract; changing a public return type is not a syntax cleanup.
- A stream backed by a resource is a resource. `Files.lines`, `Files.walk`, `Files.list`,
  `Files.find` hold open resources; JDBC/JPA result streams may hold a cursor/connection depending
  on driver/provider and execution mode. Resource-backed streams need an explicit closing owner,
  usually `try`-with-resources, documented in their Javadoc — see java-resource-management.
  Terminal traversal does not itself close the stream. When ownership transfers to a caller,
  keep the resource alive through consumption and let that owner close it, including on failure.
  `flatMap` closes each mapped stream it consumes; create those streams lazily in the mapper and
  return them open, rather than returning from inside a try-with-resources that already closes them.
  This does not close an independently resource-backed outer source. Pre-opened inner streams
  never reached by traversal still need an owner; `map` alone does not transfer closing ownership.
  If a repository stream depends on a transaction-bound cursor, consumption must finish inside
  that transaction; verify the provider contract rather than assuming every repository stream does.
- Streams are lazy: traversal work starts at a terminal operation, and short-circuiting operations
  (`findFirst`, `anyMatch`, `limit`) may stop early. `peek` is an intermediate side-effect hook,
  not a guaranteed per-source-element callback; optimization and short-circuiting may skip it.
- A bound on output is not necessarily a bound on work or memory. `sorted().limit(n)` can require
  the entire upstream input before producing a result; `filter` or `distinct` before `limit`
  may keep searching when the required matches or distinct values never arrive. Establish a
  finite source or a valid source-consumption bound when termination matters. Moving `limit`
  before a stage can change the answer: preserve whether the request concerns the whole input
  or only its first elements. A count limit is not a timeout for a blocked source or callback.
- Parallel streams commonly execute in `ForkJoinPool.commonPool()` when initiated normally;
  pool selection from custom ForkJoin tasks is implementation-sensitive, and common parallelism
  is configurable/container-aware rather than always processors-minus-one. Blocking can starve or
  distort other common-pool workloads. Parallel streams are primarily for
  CPU-bound work over a splittable source, with a measurement to show it helps.
- Virtual threads do not change a parallel stream's execution policy. For concurrent I/O per
  element, prefer explicit structured fan-out or `Gatherers.mapConcurrent`, not `parallel()`.
- Prefer `IntStream`/`LongStream`/`DoubleStream` when primitive representation matters; a
  `Stream<Integer>` carries boxed references, though traversal does not necessarily allocate new
  boxes when the source is already boxed. `mapToInt(...).sum()` and `summaryStatistics()` exist
  for exactly this.
- Use `Gatherers` (final since Java 24) for intermediate operations the JDK does not ship —
  fixed and sliding windows, `scan` for intermediate accumulations, `fold` for a final aggregate,
  and `mapConcurrent`, which runs a mapper on
  virtual threads with a concurrency limit and preserves encounter order. It is the supported
  extension point on those releases. Keep an adequate existing `Spliterator` or loop when it
  fits the source/transform contract or the supported Java baseline cannot use gatherers.

- Parallel correctness requires more than “no shared list”: reduction/collector operations need
  associative combination, a true identity, compatible accumulator/combiner behavior, and honest
  `Collector.Characteristics`. Encounter order (`findFirst`, `forEachOrdered`) can limit
  parallelism; choose `findAny`/unordered processing only when semantics permit.

Report the preserved null, duplicate, order, mutability, termination and resource-lifetime contracts, the
smallest justified rewrite (or decision to keep the loop), and tests/measurements actually run.
Do not infer a performance improvement from shorter syntax.

## References

- [Collectors and purity](references/collectors-and-purity.md) — read when choosing or
  composing collectors, when a pipeline accumulates state, when `toMap`/`groupingBy` misbehave
  on real data, or when deciding between `reduce` and `collect`.
- [Parallel streams and gatherers](references/parallel-and-gatherers.md) — read before adding
  `parallel()`, when a parallel pipeline is slower or is starving the common pool, or when a
  pipeline needs windowing, running state or bounded concurrency per element. Also read it when
  a downstream limit is being used to claim bounded work or termination.

Files in this skill

  • SKILL.md9.5 KB
  • references/collectors-and-purity.md10.4 KB
  • references/parallel-and-gatherers.md8.7 KB
  • skill.yaml2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…