انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-036-L04

flatMap

  • الدرس 4 من 8
  • 10 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

Real data is often nested. An order contains many items. A customer has many addresses. A sentence contains many words. A list of orders is therefore a list where each element holds another list. If you want "all the items across all the orders", plain map gives you the wrong shape: it hands you a list of lists, not one flat list.

flatMap is the tool for this. On the conveyor belt, imagine each arriving box is itself full of smaller parcels. A map station replaces each box with one new thing and keeps the boxes. A flatMap station opens every box and puts all the parcels on the belt, so everything downstream sees single parcels, not boxes.

Why this matters: "all products ever sold", "every tag used by any article", and "all words in a file" are one-liners with flatMap, and loops for the same jobs need a nested for. Nested collections are everywhere in backend models.

This lesson builds on Lessons 1 to 3. It uses records (previewed in BEG-026), generics (BEG-031) and method references (BEG-035).

Learning Objectives

By the end of this lesson you will be able to:

  • Explain the difference between map and flatMap using the type of the result.
  • Flatten a list of lists, or the items of many orders, into one stream.
  • Write the lambda that flatMap needs, which must return a stream.
  • Combine flatMap with filter, distinct, sorted, and count.
  • Keep a link to the parent object while flattening, by using map inside flatMap.
  • Recognise when a nested loop is clearer.

Written Explanation

The problem: map keeps the boxes

Suppose each Order has a list of item names:

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

If you write orders.stream().map(o -> o.items()), the lambda returns a List<String> for each order, so the result is a stream of lists: Stream<List<String>>. Collected, that is List<List<String>>, a list of lists. You wanted a single list of strings.

The solution: flatMap

flatMap takes a function that turns each element into a stream, then joins all those little streams into one stream:

java
List<String> allItems = orders.stream()
    .flatMap(o -> o.items().stream())
    .toList();

The function o -> o.items().stream() is called once per order, and returns that order's items as a stream. flatMap joins them. The result is Stream<String>, and toList() gives a plain List<String>.

Function returnsStream becomesOne element in gives
mapone valueStream<R>exactly one element out
flatMapa stream of valuesStream<R>zero, one, or many elements out

The key rule is in the second column: the function you give to flatMap must return a stream. If you return a List, the code will not compile; add .stream(). If you want the method reference form for a list of lists, write flatMap(List::stream).

Because the function may return an empty stream, flatMap can also drop an element entirely: an order with no items contributes nothing. The Javadoc also notes that a null returned from the function is treated as an empty stream, but do not rely on returning null; return Stream.empty() or an empty collection's stream.

Flatten, then keep working

After flatMap, the rest of the pipeline works on single items, so every earlier tool applies:

java
orders.stream()
    .flatMap(o -> o.items().stream())
    .distinct()
    .sorted()
    .toList();

This produces every distinct item name ever ordered, in alphabetical order. Replace toList() with count() after a filter and you can answer "how many pens were ordered in total?".

Splitting text

A string can be turned into a stream of its words with Arrays.stream(text.split(" ")). Combined with flatMap, a list of lines becomes a stream of words:

java
lines.stream()
    .flatMap(line -> Arrays.stream(line.split("\\s+")))
    .filter(w -> !w.isEmpty())
    .map(String::toLowerCase)
    .distinct()
    .toList();

Notice the filter for empty strings. In Java, "".split(" ") returns an array with one empty string in it, so an empty line yields an empty "word". The example in this lesson shows it in the real output.

Keeping the parent: map inside flatMap

Sometimes flattening loses information you still need. After flattening orders to items, you no longer know which customer ordered each item. To keep it, build the pair inside the function with a nested map:

java
orders.stream()
    .flatMap(o -> o.items().stream().map(item -> o.customer() + ":" + item))
    .toList();

Read it from the inside out: for each order o, turn its items into labelled strings, then flatten all those streams into one. The lambda item -> ... can use o because it sits inside the outer lambda, where o is in scope.

map versus flatMap in one question

Ask: "For each input, do I get exactly one thing, or a collection of things?" One thing: map. A collection of things that should become part of the main stream: flatMap. Another test is the type: if you see Stream<List<...>> or List<List<...>> appear and you did not want it, you probably needed flatMap.

When a loop is clearer

A flatMap followed by a map that needs the parent's fields can get tangled. If you need several nested conditions, counters, or an early exit, a nested for loop with named variables may read better. Lesson 6 gives the test: could a teammate read it quickly?

Examples

Example 1: flatten a list of lists (simple)

java
import java.util.*;

public class Flatten {
    public static void main(String[] args) {
        List<List<Integer>> nested = List.of(
            List.of(1, 2), List.of(3), List.of(), List.of(4, 5, 6));

        System.out.println(nested.stream().map(l -> l.size()).toList());
        System.out.println(nested.stream().flatMap(List::stream).toList());
    }
}

Real output:

text
[2, 1, 0, 3]
[1, 2, 3, 4, 5, 6]

The first line uses map to get each inner list's size: still one answer per inner list. The second uses flatMap to open every inner list, and the empty list simply contributes nothing.

Example 2: every item ever ordered (practical)

java
import java.util.*;

public class AllItems {
    record Order(String customer, List<String> items) { }

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("Amal", List.of("pen", "book", "pen")),
            new Order("Bo", List.of("lamp", "pen")),
            new Order("Chen", List.of()));

        List<String> all = orders.stream()
            .flatMap(o -> o.items().stream())
            .toList();
        List<String> unique = orders.stream()
            .flatMap(o -> o.items().stream())
            .distinct()
            .sorted()
            .toList();
        long pens = orders.stream()
            .flatMap(o -> o.items().stream())
            .filter(i -> i.equals("pen"))
            .count();

        System.out.println(all);
        System.out.println(unique);
        System.out.println(pens);
    }
}

Real output:

text
[pen, book, pen, lamp, pen]
[book, lamp, pen]
3

Chen's empty order does not appear and does not cause an error. Three pens were ordered in total.

Example 3: words in lines, including the empty-line trap (realistic)

java
import java.util.*;

public class Words {
    public static void main(String[] args) {
        List<String> lines = List.of("the cat sat", "the dog", "");

        System.out.println(lines.stream()
            .flatMap(l -> Arrays.stream(l.split(" ")))
            .toList());
        System.out.println(lines.stream()
            .flatMap(l -> Arrays.stream(l.split("\\s+")))
            .filter(w -> !w.isEmpty())
            .map(String::toLowerCase)
            .distinct()
            .toList());
    }
}

Real output:

text
[the, cat, sat, the, dog, ]
[the, cat, sat, dog]

In the first line, the empty last line produced an empty string (shown as a blank after the final comma). The second pipeline removes it with filter, lower-cases, and removes duplicates, giving the vocabulary of the text. The same pattern works on lines read from a file (BEG-034).

Backend Insight

Domain models are full of "one-to-many" relationships: an order has line items, a user has roles, a blog post has tags. Business questions cross them: "which roles are held by anyone in this team?", "which products appear in any order this week?". flatMap answers each in one pipeline.

When those relationships live in a relational database, the SQL equivalent is a join. Joining orders to order_items produces one row per item with the order's columns repeated, just like the map-inside-flatMap example above, which produces one customer:item pair per item. You will see this in BEG-050. A good rule: if the database can do the join and filter before the data reaches Java, let it; use flatMap for what you already hold in memory, such as the result of one query with its child collections.

Be careful with huge nested data: flattening a million orders with ten items each creates a stream of ten million elements. Streams are lazy, so it does not hold them all at once until you collect them, and collecting them does.

A practical tip for reviews: when you read a pipeline that contains flatMap, ask what the "box" and the "parcel" are. If you can say "each order is a box, each item is a parcel" in plain words, the pipeline is probably clear. If you cannot, give the lambda a clearer name or split the work into a small helper method.

Common Mistakes

Mistake 1: Using map and getting a list of lists

  • Incorrect: orders.stream().map(o -> o.items()).toList() which gives List<List<String>>.
  • Correct: orders.stream().flatMap(o -> o.items().stream()).toList().

Mistake 2: Forgetting .stream() in the lambda

flatMap(o -> o.items()) does not compile, because the function must return a stream, not a list. Add .stream().

Mistake 3: Losing the parent

After a plain flatten, you cannot tell which order an item came from. Use a nested map to carry the parent field along, or use a loop.

Mistake 4: Trusting split on an empty string

"".split(" ") gives an array with one empty string, so you may count an empty "word". Filter empty strings out.

Mistake 5: Returning null from the function

A null is treated as empty by the library, but returning it hides intent and is easy to break later. Return an empty stream instead, for example Stream.empty().

Exercises

Try each exercise yourself before opening the solutions. Use this record: record Order(String customer, List<String> items) { }.

Level A: Foundation

A1. Which operation would you use to turn List<List<String>> into one List<String>: map or flatMap?

أظهر الحل A1

A1. flatMap, using flatMap(List::stream).

A2. What type does orders.stream().map(o -> o.items()) produce: Stream<String>, Stream<Order>, or Stream<List<String>>?

أظهر الحل A2

A2. Stream<List<String>>: map keeps one output per order, and each output is a list.

A3. Given List<List<Integer>> grid = List.of(List.of(1, 2), List.of(3, 4)), write a pipeline that gives the sum of all the numbers.

أظهر الحل A3

A3.

java
int sum = grid.stream()
    .flatMap(List::stream)
    .reduce(0, Integer::sum);   // 10

Level B: Application

B1. Given List<Order> orders, write a pipeline returning the number of items ordered in total, counting duplicates.

أظهر الحل B1

B1.

java
long total = orders.stream()
    .flatMap(o -> o.items().stream())
    .count();

B2. Write a pipeline returning the distinct item names that start with "p", sorted.

أظهر الحل B2

B2.

java
List<String> p = orders.stream()
    .flatMap(o -> o.items().stream())
    .filter(i -> i.startsWith("p"))
    .distinct()
    .sorted()
    .toList();

B3. Given Map<String, List<String>> tagsByArticle, write a pipeline returning every distinct tag, sorted. (Hint: values() is a collection.)

أظهر الحل B3

B3.

java
List<String> tags = tagsByArticle.values().stream()
    .flatMap(List::stream)
    .distinct()
    .sorted()
    .toList();

Level C: Challenge

C1. Return, for each item of each order, a string customer:item, but only for items that are not "pen".

أظهر الحل C1

C1.

java
List<String> labels = orders.stream()
    .flatMap(o -> o.items().stream()
        .filter(i -> !i.equals("pen"))
        .map(i -> o.customer() + ":" + i))
    .toList();

C2. A teammate's code orders.stream().flatMap(o -> o.items()).toList() does not compile. Explain why in one sentence and give the fix. Then say whether a nested for loop would be a better choice for "print each customer followed by their items on one line", and why.

أظهر الحل C2

C2. The function passed to flatMap must return a stream, but o.items() returns a list; the fix is o.items().stream(). For "each customer followed by their items on one line", a loop is better: the output is one line per customer, so the natural shape is a for over orders with String.join on each item list, not a flat stream of items.

Mini Challenge

Invent a small nested data set, for example students with a list of courses, or blog posts with a list of tags (five or six entries, with repeats). Write three pipelines: (1) every item as one flat list, (2) the distinct items, sorted, (3) a parent:item list that keeps the link to the parent. Add one sentence saying which of the three needed a nested map and why.

Check your work: no Stream<List<...>> appears in your results, and at least one parent has an empty list to prove that case works.

Summary & Next Lesson

Key takeaways

  • map gives exactly one output per input; flatMap gives zero, one, or many.
  • The function given to flatMap must return a stream.
  • After flattening, filter, distinct, sorted, and count work on single items.
  • Use a map inside the flatMap to keep information about the parent.
  • Filter out empty strings after split.

Next lesson

The next lesson, "Collectors and grouping", teaches collect properly, including groupingBy, counting, and summingDouble, which work like SQL GROUP BY.

Self-check

  1. What is the type difference between map(o -> o.items()) and flatMap(o -> o.items().stream())?
  2. What must the function given to flatMap return?
  3. What does an empty inner list contribute after flattening?
  4. How do you keep the parent's name next to each flattened child?
  5. Why can "".split(" ") cause a surprise?
أظهر الإجابات
  1. map gives Stream<List<String>>; flatMap gives Stream<String>.
  2. A stream.
  3. Nothing: it adds no elements.
  4. Use a nested map inside the flatMap, for example o.items().stream().map(i -> o.customer() + ":" + i).
  5. It returns an array holding one empty string, so you may get an empty "word".

References

  • Stream, Java SE 21 API. Supports: flatMap replaces each element with the contents of a mapped stream, and a null mapped stream is treated as empty.
  • The behavior of "".split(" ") returning a one-element array, the real output shown, and the nested map pattern were verified by compiling and running the examples with JDK 21; String.split was not checked against a fetched page. The box-and-parcel picture is this course's own analogy.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.