Homeβ€Ί Javaβ€Ί Java flatMap(): Flatten Streams and Optional

Java flatMap(): Flatten Streams and Optional

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: Collections β†’ Topic 18 of 21
Master Java flatMap() for Stream and Optional.
βš™οΈ Intermediate β€” basic Java knowledge assumed
In this tutorial, you'll learn:
  • flatMap() is map() followed by flatten β€” use it when your mapping function produces a Stream or Optional and you don't want nesting.
  • Stream.flatMap(list -> list.stream()) is the standard idiom for flattening a list of lists into a single stream.
  • Optional.flatMap() chains Optional-returning methods without creating Optional<Optional<T>>.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
map() transforms each element to something β€” including possibly another stream or Optional. flatMap() transforms each element to a stream and then flattens all those streams into one. The 'flat' in flatMap means 'collapse the nesting'. If map() gives you a Stream<Stream<String>>, flatMap() gives you a Stream<String>.

flatMap() is the operation that unlocks real stream-based data processing. Once you understand why map() sometimes gives you nested types and how flatMap() collapses them, a whole class of multi-level data operations becomes straightforward. I've seen developers write manual loops to process nested lists because they didn't know flatMap() existed, and seen others abuse it by using it where a simple map() would suffice.

Stream flatMap() vs map(): Flattening Nested Lists

FlatMapExample.java Β· JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344
package io.thecodeforge.collections;

import java.util.List;
import java.util.stream.Collectors;

public class FlatMapExample {

    record Order(String customerId, List<String> items) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("cust-1", List.of("PaymentService", "OrderService")),
            new Order("cust-2", List.of("AuditService")),
            new Order("cust-3", List.of("PaymentService", "AuditService", "ReportService"))
        );

        // map() gives Stream<List<String>> β€” nested, can't iterate directly
        orders.stream()
            .map(Order::items)            // Stream<List<String>>
            .forEach(System.out::println);
        // [PaymentService, OrderService]
        // [AuditService]
        // [PaymentService, AuditService, ReportService]

        System.out.println("---");

        // flatMap() flattens β€” gives Stream<String>
        orders.stream()
            .flatMap(order -> order.items().stream())  // Stream<String>
            .distinct()
            .sorted()
            .forEach(System.out::println);
        // AuditService
        // OrderService
        // PaymentService
        // ReportService

        // Count total items across all orders
        long totalItems = orders.stream()
            .flatMap(o -> o.items().stream())
            .count();
        System.out.println("Total items: " + totalItems); // 6
    }
}
β–Ά Output
[PaymentService, OrderService]
[AuditService]
[PaymentService, AuditService, ReportService]
---
AuditService
OrderService
PaymentService
ReportService
Total items: 6

Optional flatMap(): Chaining Optional Operations

Optional.flatMap() solves the Optional<Optional<T>> nesting problem. When a method returns Optional<T> and you call map() with a function that also returns Optional<T>, you get Optional<Optional<T>>. flatMap() flattens it to Optional<T>.

OptionalFlatMapExample.java Β· JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344
package io.thecodeforge.collections;

import java.util.Optional;

public class OptionalFlatMapExample {

    record Customer(String id, String email) {}
    record PaymentProfile(String customerId, String tier) {}

    Optional<Customer> findCustomer(String id) {
        return "cust-42".equals(id)
            ? Optional.of(new Customer("cust-42", "alice@thecodeforge.io"))
            : Optional.empty();
    }

    Optional<PaymentProfile> findProfile(String customerId) {
        return "cust-42".equals(customerId)
            ? Optional.of(new PaymentProfile("cust-42", "GOLD"))
            : Optional.empty();
    }

    void run() {
        // map() + findProfile returns Optional<Optional<PaymentProfile>> β€” wrong
        // findCustomer("cust-42")
        //     .map(c -> findProfile(c.id()))  // Optional<Optional<PaymentProfile>>

        // flatMap() flattens Optional<Optional<T>> to Optional<T>
        Optional<String> tier = findCustomer("cust-42")
            .flatMap(c -> findProfile(c.id()))  // Optional<PaymentProfile>
            .map(PaymentProfile::tier);          // Optional<String>

        System.out.println(tier.orElse("STANDARD")); // GOLD

        // Chain for a missing customer
        Optional<String> missingTier = findCustomer("unknown")
            .flatMap(c -> findProfile(c.id()))
            .map(PaymentProfile::tier);
        System.out.println(missingTier.orElse("STANDARD")); // STANDARD
    }

    public static void main(String[] args) {
        new OptionalFlatMapExample().run();
    }
}
β–Ά Output
GOLD
STANDARD
MethodInputOutputUse When
Stream.map()T β†’ RStream<R>1-to-1 transformation
Stream.flatMap()T β†’ Stream<R>Stream<R> (flattened)1-to-many or nested list flattening
Optional.map()T β†’ ROptional<R>Transform Optional value
Optional.flatMap()T β†’ Optional<R>Optional<R> (flattened)Chain methods that return Optional

🎯 Key Takeaways

  • flatMap() is map() followed by flatten β€” use it when your mapping function produces a Stream or Optional and you don't want nesting.
  • Stream.flatMap(list -> list.stream()) is the standard idiom for flattening a list of lists into a single stream.
  • Optional.flatMap() chains Optional-returning methods without creating Optional<Optional<T>>.
  • The rule: if map() gives you Stream<Stream<T>> or Optional<Optional<T>>, you should have used flatMap().

⚠ Common Mistakes to Avoid

  • βœ•Using map() when the mapping function returns a Stream or Optional β€” this creates Stream<Stream<T>> or Optional<Optional<T>>. Use flatMap() to avoid the nesting.
  • βœ•Using flatMap() when you only need map() β€” if your mapping function returns a plain value (not a Stream or Optional), map() is correct and flatMap() would require wrapping in Stream.of().
  • βœ•Forgetting .stream() in the lambda: .flatMap(order -> order.items()) instead of .flatMap(order -> order.items().stream()) β€” the lambda must return a Stream.

Interview Questions on This Topic

  • QWhat is the difference between Stream.map() and Stream.flatMap()?
  • QGiven a List<List<String>>, how do you get a flat List<String> using streams?
  • QWhen would you use Optional.flatMap() instead of Optional.map()?

Frequently Asked Questions

What does flatMap() do in Java streams?

flatMap() applies a function to each element that produces a Stream, then flattens all those streams into a single stream. It's equivalent to map() followed by flatten. Use it when each element maps to multiple values (a list of items per order, lines per file) and you want to work with all values in a single flat stream.

What is the difference between map() and flatMap() in Java?

map() transforms each element 1-to-1. flatMap() transforms each element to a stream and then flattens those streams. If your mapping function returns a plain value, use map(). If it returns a Stream or Optional, use flatMap() to avoid nested types.

πŸ”₯
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousIdentityHashMap in JavaNext β†’HashMap vs Hashtable in Java
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged