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.
On this page
- Picture it first: keep, relabel, then flatten boxes
- filter under the hood: a gate with a predicate
- map: one input becomes one output
- 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
🧩 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)
- Pipeline laziness and vertical execution: How a stream pipeline executes
- Operation categories and one-shot stream reuse: Intermediate vs terminal operations
- Optional basics: A guide to Java Optional
- Grouping after a stream: Grouping and partitioning
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
filterwith lambdas and method references without hiding business rules - Explain
mapas a one-to-one transformation andflatMapas zero-or-more flattening - Flatten nested collections, order lines and
Optionalvalues withflatMapandOptional::stream - Choose
mapMultiwhen a small imperative expansion is clearer or avoids creating many tiny streams - Handle
if/elselogic with ternaries,partitioningBy, separate filters or extracted methods - Compare
mapversusflatMapon bothStreamandOptional
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.
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.
filterdoes not change the element type. We still haveOrderuntilmap(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.
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