Intermediate Stream Operations in Java

filter, map, flatMap, sorted, distinct, limit, skip and peek. Each returns a new stream and none of them runs on its own.

What makes an operation intermediate

An intermediate operation returns another Stream, so calls can be chained. None of them executes until a terminal operation is added.

OperationPurposeStateful
filterKeep matching elementsNo
mapTransform each elementNo
flatMapFlatten nested structuresNo
peekObserve elements as they passNo
distinctRemove duplicatesYes
sortedOrder elementsYes
limitTake the first nShort circuiting
skipDiscard the first nStateful
takeWhile, dropWhileTake or drop while a condition holdsShort circuiting
mapMultiEmit zero or more per elementNo

A stateful operation needs to see more than the current element. sorted must buffer everything before it can emit anything, which is why an infinite stream cannot be sorted.

filter

List<Note> popular = notes.stream()
        .filter(Note::published)
        .filter(note -> note.views() > 300)
        .toList();

Two filters or one combined predicate behave the same. Separate filters usually read better, and the cost is negligible because elements flow one at a time.

map

List<String> titles = notes.stream()
        .map(Note::title)
        .map(String::strip)
        .map(String::toUpperCase)
        .toList();

List<Integer> lengths = titles.stream().map(String::length).toList();
IntStream primitive = titles.stream().mapToInt(String::length);   // avoids boxing

map is one to one: every input produces exactly one output, and the element type may change.

flatMap

List<List<String>> tagsPerNote = List.of(
        List.of("java", "oop"),
        List.of("java", "streams"),
        List.of("sql"));

List<String> allTags = tagsPerNote.stream()
        .flatMap(List::stream)        // Stream<List<String>> becomes Stream<String>
        .distinct()
        .sorted()
        .toList();

System.out.println(allTags);   // [java, oop, sql, streams]
// map would give the wrong shape
Stream<Stream<String>> wrong = tagsPerNote.stream().map(List::stream);
Use map when each element becomes one value, and flatMap when each element becomes a stream of values that should be merged into one flat stream. Splitting text into words is the classic case.
List<String> words = List.of("java streams", "sql joins").stream()
        .flatMap(line -> Arrays.stream(line.split(" ")))
        .toList();          // [java, streams, sql, joins]

distinct

List<String> unique = Stream.of("a", "b", "a", "c", "b")
        .distinct()
        .toList();          // [a, b, c]

Duplicates are decided by equals, so the element type must implement it sensibly. Records do this automatically.

sorted

List<Note> ordered = notes.stream()
        .sorted(Comparator.comparingInt(Note::views).reversed()
                .thenComparing(Note::title))
        .toList();

List<String> natural = titles.stream().sorted().toList();   // needs Comparable

limit and skip

List<Note> topThree = notes.stream()
        .sorted(Comparator.comparingInt(Note::views).reversed())
        .limit(3)
        .toList();

List<Note> page2 = notes.stream()
        .skip(20)
        .limit(10)
        .toList();          // simple pagination

limit short circuits: once it has enough elements the pipeline stops pulling from the source, which is how an infinite stream can be used safely.

takeWhile and dropWhile

List<Integer> values = List.of(1, 3, 5, 8, 9, 11);

List<Integer> leading = values.stream()
        .takeWhile(n -> n % 2 == 1)
        .toList();          // [1, 3, 5] - stops at the first even value

List<Integer> rest = values.stream()
        .dropWhile(n -> n % 2 == 1)
        .toList();          // [8, 9, 11] - keeps everything after

These differ from filter: they stop at the first element that fails, rather than testing every element. Introduced in Java 9.

peek

List<String> result = notes.stream()
        .peek(note -> logger.fine("considering " + note.title()))
        .filter(Note::published)
        .map(Note::title)
        .toList();

peek is for debugging and logging only. It is not a hook for modifying elements, and the specification allows an implementation to skip it entirely when the result does not depend on it.

Order of operations matters

// Sorts everything, then discards most of it
notes.stream().sorted(byViews).filter(Note::published).limit(5).toList();

// Filters first, so far less is sorted
notes.stream().filter(Note::published).sorted(byViews).limit(5).toList();

Put cheap, reducing operations such as filter early, and expensive stateful ones such as sorted late.

Common mistakes

  • Using map where flatMap was needed, and ending with a stream of streams.
  • Calling sorted on an infinite stream.
  • Using peek to modify elements or accumulate results.
  • Placing sorted before filter and sorting data that is about to be discarded.
  • Expecting distinct to work on a class without equals and hashCode.
  • Confusing takeWhile with filter.

Best practices

  • Filter early, sort late.
  • Use method references where the lambda only delegates.
  • Prefer primitive streams for numeric transformations.
  • Keep every lambda free of side effects.
  • Break a very long pipeline into named intermediate variables or methods.

Practice

  1. Flatten a list of lists of tags into one sorted list of unique tags.
  2. Explain the difference in output between filter and takeWhile on the list 1, 3, 5, 8, 9.
  3. Implement pagination for page 3 with 10 items per page.
  4. Why is sorted before filter usually the wrong order?
  5. Split a paragraph into unique lower case words in one pipeline.

Conclusion

Intermediate operations build the pipeline without running it. Know which are stateful, use flatMap when each element expands into many, and order operations so the cheap ones reduce the work for the expensive ones.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.