Back to Blog
NewsOctober 24, 20269 min read

Java Streams Beyond map() and filter(): 8 Methods

Most people stop at map(), filter() and collect(). Eight documented Stream and Collectors methods, with Java version tags, replace a lot of loop code.

Java Streams Beyond map() and filter(): 8 Methods

6 min read · Last updated: October 25, 2026

Java Streams go well beyond map() and filter(). The Java standard library also ships groupingBy, partitioningBy, teeing, flatMapping, takeWhile, a three-argument iterate, mapMulti and toList. Each replaces a hand-written loop with one expression, and each comes with rules the Oracle docs spell out.

Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.

The post that prompted this

The docs

Key takeaways

  • Collectors.teeing (Java 12) feeds every element to two collectors and merges the results.
  • takeWhile, dropWhile, flatMapping, filtering and the three-argument iterate all arrived in Java 9.
  • Stream.toList() and mapMulti arrived in Java 16. toList() returns an unmodifiable list.
  • takeWhile on an unordered stream is nondeterministic. Only ordered streams give you the longest matching prefix.
  • The docs discourage side effects in stream operations. Collect with toList() instead of forEach(list::add).

What happened?

On Medium, Sanid Andwate published "Java Streams in Practice: Beyond map() and filter()". Its teaser says Java Streams are easy to start using: you can filter a collection, transform its elements and collect the result in a few lines.

I could not open the full post, because Medium returned an HTTP 403 to my fetch. So I am not summarising it. I took its theme, what comes after map() and filter(), and checked every method below against Oracle's own Java SE 22 documentation.

Why does this matter to a developer?

Most stream code stops at filter, map and collect. The next step in the same API removes the loops that remain: grouping, splitting, early exit, and two results from one pass. Learning it means less boilerplate, and it means knowing the rules that make streams safe.

The Java docs are blunt about one of them. They say side effects in stream operations are "in general, discouraged" because they can break the statelessness requirement. Knowing the richer methods is how you avoid reaching for them.

Screenshot: Oracle's Java SE 22 Collectors API page listing groupingBy, partitioningBy and teeing

What changes in practice?

Eight methods cover most of it. The table shows what each one does and when it arrived, per the Oracle Java SE 22 docs.

MethodWhat it doesSince
Collectors.groupingByGroups elements by a classification function into a Map1.8 (class)
Collectors.partitioningBySplits elements by a predicate into Map<Boolean, List<T>>1.8 (class)
Collectors.teeingRuns two collectors on the same elements, then merges12
Collectors.flatMapping / filteringMap or filter inside a downstream collector9
Stream.takeWhile / dropWhileTake or skip the leading run that matches9
Stream.iterate(seed, hasNext, next)A stream that behaves like a for loop9
Stream.mapMultiReplace each element with zero or more elements16
Stream.toList()Unmodifiable list in encounter order16

The docs list groupingBy and partitioningBy under the Collectors class, which is Since 1.8.

How do you group and split a stream?

Use groupingBy to group elements by a key, and partitioningBy to split them in two by a predicate. Per the docs, groupingBy returns a Map keyed by your classification function. partitioningBy always returns both a true and a false key, even when a side is empty.

List<String> words = List.of("java", "go", "rust", "c", "kotlin");

Map<Integer, Long> byLength = words.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

Map<Boolean, List<String>> longOrShort = words.stream()
    .collect(Collectors.partitioningBy(w -> w.length() > 3));

That "always both keys" rule matters. You can call longOrShort.get(false) without a null check. If a partition has no elements, the docs say its value is an empty List.

What is Collectors.teeing?

Collectors.teeing, added in Java 12, returns a collector built from two downstream collectors. The docs say every element goes to both, and a merge function combines their results. It gives you two answers from one pass over the data, without a temporary list.

String summary = words.stream().collect(Collectors.teeing(
    Collectors.counting(),
    Collectors.joining(", "),
    (count, joined) -> count + " words: " + joined));

Before teeing, this usually meant collecting into a list first, then streaming it twice.

How do flatMapping and filtering work inside groupingBy?

Collectors.flatMapping and Collectors.filtering, both Since 9, adapt a downstream collector. filtering accumulates an element only if a predicate returns true. flatMapping applies a flat mapping function to each element before accumulation. They apply per group, which a plain filter() before groupingBy cannot do.

The difference is visible with empty groups. Filtering before grouping removes a key entirely. Filtering inside the group keeps the key with an empty collection.

Map<Integer, List<String>> longOnly = words.stream().collect(
    Collectors.groupingBy(String::length,
        Collectors.filtering(w -> w.startsWith("k"), Collectors.toList())));

I am inferring the "key stays" behaviour from how a downstream collector works, not from a quoted line in the docs, so run it on your own data before you rely on it.

When should you use takeWhile and dropWhile?

takeWhile and dropWhile, Since 9, work on the leading run of a stream. For an ordered stream, takeWhile returns the longest prefix that matches the predicate. dropWhile returns what remains after dropping that prefix. They stop early, unlike filter(), which tests every element.

There is a trap. For an unordered stream, the docs say that if only some elements match, the behaviour is nondeterministic. takeWhile is then free to take any subset of the matches, including none.

List<Integer> sorted = List.of(1, 2, 3, 10, 4, 5);
List<Integer> head = sorted.stream().takeWhile(n -> n < 5).toList();

Here head is [1, 2, 3], because the run stops at 10. A filter(n -> n < 5) would also have kept 4.

How does the three-argument iterate replace a for loop?

Stream.iterate(seed, hasNext, next), Since 9, returns a sequential, ordered stream that ends as soon as hasNext returns false. The docs say it should produce the same sequence as the equivalent for (T i = seed; hasNext.test(i); i = next.apply(i)) loop.

List<Integer> powers = Stream.iterate(1, n -> n < 200, n -> n * 2).toList();

The older two-argument iterate is infinite and needs limit(). The three-argument form has its own stop condition.

When is mapMulti better than flatMap?

mapMulti, Since 16, replaces each element with zero or more elements through a consumer you call. The docs say it is preferable to flatMap when you replace each element with a small number of results, because it avoids creating a new Stream for every element.

List<Integer> doubledOdds = Stream.of(1, 2, 3, 4, 5)
    .<Integer>mapMulti((n, sink) -> {
        if (n % 2 == 1) { sink.accept(n); sink.accept(n); }
    })
    .toList();

For large or lazy inner collections, flatMap is still the better fit.

What does Stream.toList() return?

Stream.toList(), Since 16, accumulates elements into a list in encounter order. The returned list is unmodifiable, and calling any mutator throws UnsupportedOperationException, per the docs. If you need a mutable result, the docs point you to Collectors.toCollection(Supplier).

The package summary uses it to show the right pattern. Instead of adding to an outside ArrayList inside forEach, it chains filter(...) into toList().

What are the common mistakes with Java Streams?

The docs name four. Avoid them and most stream bugs go away.

MistakeWhat the docs say
Mutating outside state in forEachSide effects are discouraged and can break statelessness
Stateful lambdasResults "may be nondeterministic or incorrect"
Reusing a streamA stream should be operated on only once and may throw IllegalStateException
Modifying the source mid-pipelineNon-interference applies to all pipelines, not just parallel ones

One more from the docs: peek "exists mainly to support debugging". Do not put real logic in it.

What should you do this week?

  1. Search your codebase for forEach calls that add to a list or map. Rewrite each with toList() or a collector.
  2. Find any place that builds a Map<Boolean, ...> by hand. Swap it for partitioningBy.
  3. Find any place that streams a list twice for two results. Try teeing.
  4. Replace one for (int i = ...; cond; i = ...) loop that builds a list with three-argument iterate.
  5. Check which Java version your project targets. Everything above works from Java 16 onward, and most of it from Java 9.

FAQ

What Java version do I need for teeing, mapMulti and toList()?

Collectors.teeing needs Java 12. Stream.mapMulti and Stream.toList() need Java 16. takeWhile, dropWhile, flatMapping, filtering and the three-argument iterate need Java 9, according to the Oracle Java SE 22 documentation.

Is a Java stream faster than a for loop?

This article does not make that claim, and the Oracle docs I used do not either. Streams are about expressing the pipeline clearly. If speed matters, measure your own workload.

Can I reuse a Java stream?

No. The docs say a stream should be operated on only once, and an implementation may throw IllegalStateException if it detects reuse. Create a new stream from the source each time.

Does parallel change the result of a stream?

It should not. The package summary says that, except for explicitly nondeterministic operations such as findAny(), serial or parallel execution should not change the result. That depends on your functions being stateless and, for reduce, associative.

Pick one loop in your code this week and rewrite it with a method from the table, then try the free tools on Art of Code. For more on the craft side, read Code Was Never the Hard Part, or set up your editor with 10 VS Code Extensions Every Developer Needs in 2026.

Written by Adithya Guttha, Founder of Art of Code.

#java#java streams#collectors#java 9#java 16

Sign in to like and save posts.

Curious Adithya

Written by

Curious Adithya

@curious_adithya · 5 followers

Discussion

Comments (0)

Want to join the conversation?

Sign in to comment

No comments yet. Be the first to comment!