🌊 Streams API · Intermediate

filter & map in Java

Selecting and transforming elements.

🧩 The mysteryYour teammate writes .filter(String::isEmpty) to "remove the empty strings". The result contains ONLY empty strings. Who's right about what filter does?

The bouncer and the transformer

**filter(predicate) is a bouncer: it keeps** the elements for which the predicate returns true and drops the rest. **map(function) is a transformer: one element in, one element out**, every element converted.

map can change the type

map takes a Function<T, R>, so the output type R can differ from the input. Mapping words to their lengths turns a Stream<String> into a Stream<Integer>.

List<Integer> lengths = words.stream()
    .map(String::length) // String→Integer
    .toList();
🔮 Predict it

Bounce, then transform

What does this print?

List<Integer> r = Stream.of(1, 2, 3, 4, 5, 6)
    .filter(n -> n > 3)
    .map(n -> n + 100)
    .toList();
System.out.println(r);
  1. [104, 105, 106]
  2. [101, 102, 103]
  3. [4, 5, 6]
Show the answer

[104, 105, 106] — filter keeps 4, 5, 6, then map adds 100 to each survivor.

🤔 Think first

Does order matter?

On 1, 2, 3: is map(n -> n * 2).filter(n -> n > 4) the same as filter(n -> n > 4).map(n -> n * 2)?

Think about it, then reveal the answer

No! Map first gives 2, 4, 6 → filter keeps [6]. Filter first sees 1, 2, 3 → nothing is > 4 → []. Each operation receives the output of the operation before it.

⚠️ The trap

filter KEEPS, it doesn't remove

This keeps only the empty strings — the opposite of the goal. To keep the non-empty ones, invert the test: **Predicate.not(String::isEmpty)** or s -> !s.isEmpty().

items.stream()
    .filter(String::isEmpty) // keeps ""!
    .toList();
items.stream()
    .filter(Predicate.not(String::isEmpty))
    .toList();                 // correct
💼 In the real world

The bread and butter of real code

Most business logic is filter-then-map: active users → their emails, paid orders → their totals. Reviewers watch for inverted predicates (keeping what you meant to drop) — a bug that compiles fine and quietly ships wrong data.

Key takeaways

  1. filter(pred): keeps elements where pred is true
  2. map(fn): one output for each input, type may change
  3. Order matters: each op sees the previous op's output
  4. Predicate.not(String::isBlank) inverts a method reference
🤯 Did you know?

Predicate.not only arrived in Java 11 — before that, flipping a method reference meant writing a lambda like s -> !s.isBlank() by hand.

Practice questions

What does this print?

List<Integer> r = Stream.of(1, 2, 3, 4, 5)
    .filter(n -> n % 2 == 1)
    .map(n -> n * 10)
    .toList();
System.out.println(r);
  1. [20, 40]
  2. [10, 20, 30, 40, 50]
  3. [1, 3, 5]
  4. [10, 30, 50]
Check your answer

[10, 30, 50]. filter keeps the odd numbers 1, 3, 5, then map multiplies each by 10.

What does this print?

List<Integer> r = Stream.of(1, 2, 3, 4)
    .map(n -> n * 3)
    .filter(n -> n % 2 == 0)
    .toList();
System.out.println(r);
  1. [6, 12]
  2. [2, 4]
  3. [3, 9]
  4. [6, 12, 18]
Check your answer

[6, 12]. map runs first, producing 3, 6, 9, 12. filter then keeps the even ones: 6 and 12.

Next: what if each element contains *several* values — lines full of words, orders full of items? map gives you boxes inside boxes…