Java Stream API

Stop writing loops to filter, transform, and summarise data. Learn to build readable stream pipelines that say what you want, not how to loop for it.

Learn Java Stream API in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

You should be comfortable with Collections ( List , Map ) and the basics of lambda expressions — the short x -> x * 2 functions you pass to stream operations. A stream is built on top of a collection, so if you can create a List String you're ready.

A stream pipeline works exactly like a factory assembly line. Raw materials (your collection) enter at one end. They pass through a series of stations — one keeps only the good parts ( filter ), one reshapes each part ( map ), one puts them in order ( sorted ). At the very end, a worker boxes up the finished goods ( collect ).

💡 The key insight: the conveyor belt does not move until someone at the end asks for a finished product. The middle stations (intermediate operations) are just instructions written on a clipboard. Only the final terminal operation switches the belt on. That is lazy evaluation , and it is what makes streams efficient.

A stream is a one-shot sequence of elements you push through a pipeline. You don't store data in a stream — you flow data through one. There are three common ways to start a stream:

Intermediate operations each take a stream and return a new stream, so you can chain them. They are lazy — calling them just records a step; no data moves yet. The ones you'll reach for constantly:

A terminal operation is what finally runs the pipeline and produces a result. After it runs, the stream is consumed. The common ones:

The real power lives in the Collectors class, which you pass to collect :

Finish the pipeline below. Fill in the filter predicate so only numbers greater than 10 survive. The expected output is in the comment — run it on your machine and check.

This time fill in two blanks: the intermediate operation that transforms each element, and the expression that gives a word's length. Compare with the expected output in the comment.

Lazy evaluation means the pipeline does nothing until a terminal operation pulls data through. Intermediate operations are fused into a single pass, so .filter().map().filter() does not create three temporary lists — each element flows through the whole chain at once. Short-circuiting operations like limit and findFirst can stop early, so the source is never fully processed.

A parallel stream ( .parallel() or collection.parallelStream() ) splits the work across CPU cores using the shared ForkJoinPool. It can be dramatically faster — but only sometimes.

Time to fly solo. The starter below has only a comment outline — no filled-in logic. Build a single pipeline that finds the customers who spent $100 or more, sorted alphabetically. The expected output is in the comments.

You can now build complete stream pipelines — create a stream with .stream() , Stream.of , or IntStream.range , chain intermediate ops like filter and map , and finish with a terminal op such as collect , reduce , or toList . You also understand lazy evaluation and when parallel streams help.

Next: Lambda Expressions Deep Dive — functional interfaces, method references, and the lambdas that power every stream operation you just learned.

Practice quiz

What does List.of("Alice","Bob","Charlie","Bob").stream().filter(n -> n.length() > 3).distinct().map(String::toUpperCase).sorted().toList() produce?

filter drops "Bob" (length 3), distinct removes the duplicate, map uppercases, sorted orders them: [ALICE, CHARLIE].

What is the difference between an intermediate and a terminal operation?

  • Intermediate ops return a new stream and are lazy; the terminal op runs the pipeline and produces a result
  • Intermediate ops run immediately; terminal ops are lazy
  • There is no difference
  • Terminal ops can be chained; intermediate ops cannot

Answer: Intermediate ops return a new stream and are lazy; the terminal op runs the pipeline and produces a result. Intermediate ops (filter, map, sorted) are lazy and just describe the pipeline. A terminal op (collect, reduce, toList) actually runs it.

What does IntStream.range(1, 4).sum() return?

  • 10
  • 4
  • 3
  • 6

Answer: 6. range's end is EXCLUSIVE, so it streams 1, 2, 3 (not 4). 1 + 2 + 3 = 6.

What does List.of(1,2,3,4,5).stream().map(n -> n * 2).reduce(0, Integer::sum) return?

  • 15
  • 30
  • 120
  • 10

Answer: 30. map doubles each element to 2,4,6,8,10, then reduce folds them starting at 0: 2+4+6+8+10 = 30.

Why can't you reuse a stream after calling a terminal operation?

  • A stream is a one-shot pipeline; once consumed it throws IllegalStateException
  • Streams are thread-local
  • The garbage collector deletes it
  • You can reuse it freely

Answer: A stream is a one-shot pipeline; once consumed it throws IllegalStateException. A stream is single-use. Operating on a consumed stream throws 'stream has already been operated upon or closed'. Make a fresh one from the source.

Which terminal operation gathers stream elements into a new List (Java 16+)?

  • .forEach()
  • .filter()
  • .toList()
  • .map()

Answer: .toList(). toList() (Java 16+) is a shortcut that collects into an unmodifiable List. filter and map are intermediate ops.

When are parallel streams actually worth using?

  • For any stream, always
  • For large datasets with stateless, CPU-bound work
  • Only for tiny collections
  • Whenever you share mutable state

Answer: For large datasets with stateless, CPU-bound work. Parallel streams help only for large datasets and stateless, CPU-bound work. For small collections the splitting overhead makes them slower.

What does Collectors.joining(", ", "[", "]") do to a stream of name strings?

  • Sorts the names
  • Removes duplicates
  • Counts the names

joining(delimiter, prefix, suffix) concatenates with ", " between elements and wraps with [ ... ], e.g. [Apple, Banana].

What does a stream do if you forget the terminal operation, e.g. list.stream().filter(...).map(...);?

  • It runs the pipeline anyway
  • Nothing runs - intermediate ops are lazy
  • It throws a compile error
  • It prints each element

Answer: Nothing runs - intermediate ops are lazy. Intermediate ops are lazy. Without a terminal op like collect, forEach, or toList, the pipeline never executes.

What is Collectors.groupingBy most like?

  • SQL ORDER BY
  • SQL SELECT *
  • SQL GROUP BY
  • SQL DELETE

Answer: SQL GROUP BY. groupingBy(classifier) buckets elements by a key, exactly like SQL GROUP BY, producing a Map of key to list of elements.