Course contents

filter, map and flatMap

filter with lambdas and method references, map one-to-one transforms, flatMap one-to-many flattening, mapMulti, Optional map vs flatMap and stream-friendly conditional logic.

PremiumMust know for interviewsLesson15 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: keep, relabel, then flatten boxes
  2. filter under the hood: a gate with a predicate
  3. map: one input becomes one output
  4. flatMap: one input becomes zero, one or many outputs
  5. Optional has map and flatMap too
  6. mapMulti: the imperative cousin of flatMap
  7. If/else logic in streams without turning them into loops
  8. How to say it in the interview

🧩 What are

filter, map and flatMap? filter, map and flatMap are the three stream operations you reach for when you want to keep, transform or expand elements. filter keeps orders that match a rule, map turns each order into exactly one value like an ID, and flatMap turns each order into zero or more values like line items. Think of them as a security gate, a label printer and a box opener on the same conveyor belt.

For this lesson, we'll focus on the practical stream operations interviewers expect you to use fluently: filter with lambdas and method references, map as a one-to-one transform, flatMap for nested lists and order lines, Optional::stream, mapMulti since Java 16, and the right way to express small if/else decisions inside a pipeline. Interviewers ask this because weak answers often know the method names but cannot explain the shape change.

Below the line (out of scope)

The 30-second answer

filter takes a Predicate<T> and keeps only elements where the predicate returns true. Use a lambda when the rule is local, like order -> order.cents() > 1000, and a method reference when the domain method already says the rule, like Order::paid. map is one-to-one: each input produces exactly one output, so Stream<Order> can become Stream<String> of order IDs. flatMap is one-to-many or zero-to-many: each input produces a stream, and Java flattens those streams into one result stream. Use it for List<List<T>>, orders -> lines, and Optional::stream. Since Java 16, mapMulti is a lower-allocation alternative when emitting zero or more items imperatively. For conditional logic, prefer a ternary in map, partitioningBy, two named filters or an extracted method; a huge if/else inside forEach is usually a loop pretending to be a stream.

What you’ll learn

  • Use filter with lambdas and method references without hiding business rules
  • Explain map as a one-to-one transformation and flatMap as zero-or-more flattening
  • Flatten nested collections, order lines and Optional values with flatMap and Optional::stream
  • Choose mapMulti when a small imperative expansion is clearer or avoids creating many tiny streams
  • Handle if/else logic with ternaries, partitioningBy, separate filters or extracted methods
  • Compare map versus flatMap on both Stream and Optional

01Picture it first: keep, relabel, then flatten boxes

Picture a warehouse belt. The first worker removes unpaid orders, the second worker prints one shipping label for each remaining order, and the third worker opens each order box and sends every line item forward. That third worker is the shape change people miss: one box can produce three line items, and an empty box can produce none.

Ask what shape comes out. If the answer is one output per input, reach for map. If the answer is zero or more outputs per input, reach for flatMap or sometimes mapMulti.

Fig. 1

A stream of three orders goes through map to produce three labels, while flatMap opens each order into a single stream of line items with zero, one or many outputs per order

A stream of three orders goes through map to produce three labels, while flatMap opens each order into a single stream of line items with zero, one or many outputs per order

02filter under the hood: a gate with a predicate

filter is the easiest operation to read and one of the easiest to abuse. A predicate should answer one yes/no question. When that question already has a domain name, I prefer the method reference because it reads like a sentence.

Try it yourself first

Before reading the output, decide which orders survive each filter and why the two pipelines return the same IDs.

import java.util.List;

record Order(String id, int cents, boolean paid) {
    boolean isLargePaidOrder() {
        return paid && cents >= 1_000;
    }
}

final class FilterDemo {
    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("o-1", 1200, true),
                new Order("o-2", 700, true),
                new Order("o-3", 4000, false)
        );

        List<String> withLambda = orders.stream()
                .filter(order -> order.paid() && order.cents() >= 1_000)
                .map(Order::id)
                .toList();

        List<String> withMethodReference = orders.stream()
                .filter(Order::isLargePaidOrder)
                .map(Order::id)
                .toList();

        System.out.println(withLambda);
        System.out.println(withMethodReference);
    }
}

The output is:

[o-1]
[o-1]

Notice three things.

  • The lambda is fine when the rule is short and local.
  • The method reference is better once the rule has business meaning and a name.
  • filter does not change the element type. We still have Order until map(Order::id).

Two filters can also be clearer than one dense predicate when each rule deserves a name.

List<String> ids = orders.stream()
        .filter(Order::paid)
        .filter(order -> order.cents() >= 1_000)
        .map(Order::id)
        .toList();

This is not slower in the way people fear for normal service code. Stateless operations are fused during traversal, which we cover in How a stream pipeline executes. For 500 orders, readability beats trying to save one predicate call.

In the real world

A real code review smell is filter(order -> order.status().equals("PAID") || order.status().equals("SETTLED") || order.status().equals("CAPTURED")) copied into four services. The bug arrives when one service adds AUTHORIZED and the others do not. I would rather see Order::isChargeable and one tested domain rule.

03map: one input becomes one output

map is a transformation. The count of elements is the same as the count coming in, unless an earlier operation already removed some. The type can change, but the cardinality does not.

Try it yourself first

Before reading on, say the type after each stage: Stream<Order>, Stream<Integer> or Stream<String>?

import java.util.List;

record Order(String id, int cents, boolean paid) { }
record Receipt(String orderId, String displayTotal) { }

final class MapDemo {
    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("o-1", 1200, true),
                new Order("o-2", 700, false),
                new Order("o-3", 2500, true)
        );

        List<Receipt> receipts = orders.stream()
                .filter(Order::paid)
                .map(order -> new Receipt(order.id(), "$" + order.cents() / 100))
                .toList();

        System.out.println(receipts);
    }
}

The output is:

[Receipt[orderId=o-1, displayTotal=$12], Receipt[orderId=o-3, displayTotal=$25]]

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • flatMap: one input becomes zero, one or many outputs
  • Optional has map and flatMap too
  • mapMulti: the imperative cousin of flatMap
  • If/else logic in streams without turning them into loops
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicOperations with streams: interview questions & scenarios
Further reading (6)