skip to content

Intermediate Operations

The everyday operations — map, filter, distinct, sorted, peek, limit, skip, takeWhile and dropWhile — each returning a new stream. Interviewers expect you to know which of them are stateful and which short-circuit.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

6

Explain the difference between map and filter as stream intermediate operations, including how they affect element count and type.

level: juniorimportance: must knowfreq 80%

answer

  1. filter = 'should it stay?' (Predicate -> boolean)
  2. map = 'what does it become?' (Function -> new value)
  3. filter: count shrinks, type same
  4. map: count same, type can change
  5. Filter before map: fewer maps, avoid NPEs

basics

~20 s

filter keeps only elements that match a condition, so it can shrink the stream but never changes element type. map transforms each element into a new value (possibly a different type) one-for-one, so it keeps the same number of elements but can change what they are.

solid answer

~40 s

filter takes a Predicate<T> (a function returning boolean) and passes through only the elements for which it returns true; the element type is unchanged and the count can only stay the same or decrease. map takes a Function<T, R> and replaces each element with the function's result; it is one-to-one, so the count is always preserved, but the element type can change from T to R (e.g. String to Integer). A useful rule: filter answers 'should this element stay?', map answers 'what should this element become?'. They are commonly combined, and order matters for cost and correctness — filtering before mapping means you map fewer elements, and sometimes you must filter first to avoid NullPointerExceptions in the mapper. Neither mutates the source. For object-to-primitive transforms there are mapToInt/mapToLong/mapToDouble variants that return primitive streams.

code

java · 8 lines
java
List<String> names = Arrays.asList("alice", null, "bob");

List<Integer> lengths = names.stream()
    .filter(Objects::nonNull)        // selection: drops null  -> count shrinks
    .map(String::length)             // transform: String -> Integer (type changes)
    .collect(Collectors.toList());   // [5, 3]

// map is one-to-one: input size 2 (after filter) -> output size 2

go deeper

for a junior

Can state that filter keeps matching elements and map transforms each element, and give a small correct example of each.

for a middle

Explains the count/type contract precisely (filter: same-or-fewer, same type; map: same count, possibly new type) and knows filter takes a Predicate, map a Function.

for a senior

Discusses ordering for cost and null-safety, the distinction from flatMap, and the primitive mapToInt/mapToLong/mapToDouble variants for performance.

for a principal

Weighs readability and micro-performance trade-offs across a large codebase, and recognizes when a single map with a richer mapper, or a plain loop, communicates intent better than a long filter/map chain.

## The two questions `map` and `filter` are the two most common intermediate operations, and the clearest way to keep them straight is the question each one answers: - **`filter` asks: "Should this element stay in the stream?"** It is a *selection* / *gatekeeping* operation. - **`map` asks: "What should this element become?"** It is a *transformation* operation. ## filter in detail `Stream<T> filter(Predicate<? super T> predicate)` A **Predicate** is a function that takes one argument and returns a `boolean` — true or false. `filter` runs the predicate on every element. If it returns `true`, the element passes through; if `false`, the element is dropped. ```java List.of(1, 2, 3, 4, 5).stream() .filter(n -> n % 2 == 0) // keep evens .collect(Collectors.toList()); // [2, 4] ``` Key properties: - **Element type is unchanged.** A `Stream<Integer>` stays a `Stream<Integer>`. - **Count can only shrink or stay equal.** Filtering never adds or transforms elements. ## map in detail `<R> Stream<R> map(Function<? super T, ? extends R> mapper)` A **Function<T, R>** takes one argument of type T and returns a value of type R. `map` replaces every element with the result of applying the function. ```java List.of("a", "bb", "ccc").stream() .map(String::length) // String -> Integer .collect(Collectors.toList()); // [1, 2, 3] ``` Key properties: - **One-to-one.** Every input element produces exactly one output element — the count is **always preserved**. - **Type can change.** `Stream<String>` can become `Stream<Integer>`. This is why `map` is generic in a new type parameter `R`. ## Why count behaves differently | Operation | Can change element type? | Effect on count | |-----------|--------------------------|-----------------| | `filter` | No | Same or fewer | | `map` | Yes | Exactly the same| If you need *one element to become many* (e.g. flatten a list of lists), that is **`flatMap`**, a separate operation — `map` alone is strictly one-to-one. ## Order matters Because the pipeline is a single pass, the order of `filter` and `map` changes both cost and correctness: ```java // Filter first: map runs on fewer elements (cheaper if map is expensive) stream.filter(this::isValid).map(this::expensiveTransform); // Filtering first can also avoid errors: names.stream() .filter(Objects::nonNull) // drop nulls FIRST .map(String::toUpperCase) // safe: no NPE .collect(toList()); ``` If you mapped first and the mapper dereferenced a null, you would get a `NullPointerException`. ## Primitive variants When mapping objects to primitives, prefer `mapToInt`, `mapToLong`, `mapToDouble`. They return primitive streams (`IntStream`, etc.) that avoid boxing and unlock numeric terminal ops like `sum()` and `average()`: ```java int total = words.stream().mapToInt(String::length).sum(); ``` ## Neither mutates the source Both produce new pipeline stages; the original collection is never modified.

  • If you need one element to expand into several elements, which operation do you use?
    flatMap. map is strictly one-to-one, so it cannot increase the element count. flatMap maps each element to a stream and then flattens all those streams into one, which is how you expand or flatten nested structures.
  • Why might you put filter before map in a pipeline?
    Two reasons: performance — mapping runs on fewer elements if you discard unwanted ones first, which matters when the mapper is expensive; and correctness — filtering out nulls or invalid values first prevents the mapper from throwing (e.g. a NullPointerException).

Think of an assembly line of parcels. filter is a security gate that lets some parcels through and rejects others (same parcels, fewer of them). map is a relabeling station that swaps each parcel's contents for something new (same number of parcels, different contents).

saying these in an interview costs you the question

  • Saying map can drop elements — it is one-to-one and always preserves count
  • Saying filter can change element type — it never does
  • Confusing map (one-to-one) with flatMap (one-to-many)
  • Believing the order of filter and map never matters

context

open as a page

What are intermediate operations in the Java Streams API, and what does it mean that they are lazy?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Intermediate operations like map, filter, and sorted transform a stream and return a new stream, so you can chain them. They are lazy: nothing actually runs until you add a terminal operation (such as collect or forEach) at the end.

open as a page

What is the difference between stateful and stateless intermediate operations in the Streams API? Give examples and explain why it matters.

level: middleimportance: must knowfreq 70%

basics

~20 s

A stateless operation (like map, filter, peek) handles each element on its own without remembering others. A stateful operation (like distinct, sorted, limit, skip) needs to remember or see other elements to do its job, so it may have to buffer data. Stateful ops cost more and can hurt parallel performance.

open as a page

When and how should you use peek() and sorted() (including a custom comparator)? What are the pitfalls of each?

level: middleimportance: should knowfreq 58%

basics

~20 s

peek lets you look at each element as it flows by, mainly for debugging or logging, without changing it. sorted orders the stream, either by natural order or by a Comparator you pass in. Pitfalls: peek may not run for every element (and shouldn't change state), and sorted must buffer the whole stream and needs Comparable elements if you give no comparator.

open as a page

Compare limit/skip with takeWhile/dropWhile (Java 9). How do they behave on ordered vs unordered and infinite streams?

level: seniorimportance: should knowfreq 52%

basics

~20 s

limit(n) keeps the first n elements and skip(n) drops the first n, both by position. takeWhile keeps elements from the start as long as a condition is true and stops at the first failure; dropWhile drops the leading run that matches and keeps the rest. limit and takeWhile can stop a stream early, so they work on infinite streams; skip and dropWhile generally must process the leading part.

open as a page

How does the Streams runtime fuse intermediate operations into a single pass, and how should operation ordering, statefulness, and short-circuiting inform how you assemble a pipeline?

level: principalimportance: should knowfreq 40%

basics

~20 s

Stream operations don't each loop over the data separately. The runtime links the stateless steps into one pass where each element flows all the way through before the next starts. Knowing this, you put cheap and shrinking operations (filter, limit) early, and place stateful barriers (sorted, distinct) carefully so they handle the least data.

open as a page