Java 8 Streams: Definitive Guide to flatMap()

Introduction

Mapping elements from one collection to another, applying a transformative function between them is a fairly common and very powerful operation. Java's functional API supports both map() and flatMap().

If you'd like to read more about map(), read our Java 8 - Stream.map() Examples!

The flatMap() operation is similar to map(). However, flatMap() flattens streams in addition to mapping the elements in those streams.

Flat mapping refers to the process of flattening a stream or collection from a nested/2D stream or collection into their 1D representation:

List of lists: [[1, 2, 3], [4, 5, 6, 7]]
Flattened list: [1, 2, 3, 4, 5, 6, 7]

For example, let us say we have a collection of words:

Stream<String> words = Stream.of(
    "lorem", "ipsum", "dolor", "sit", "amet"
);

And, we want to generate a list of all the Character objects in those words. We could create a stream of letters for each word and then combine these streams into a single stream of Character objects.

First, let's try using the map() method. Since we want to chain two transformative functions, let's define them upfront instead of anonymously calling them as Lambda Expressions:

// The member reference replaces `word -> word.chars()` lambda
Function<String, IntStream> intF = CharSequence::chars;

This function accepts a String and returns an IntStream - as indicated by the types we've passed in. It transforms a string into an IntStream.

Note: You can represent char values using int values. Thus, when you create a stream of primitive char values, the primitive stream version of int values (IntStream) is preferable.

Now, we can take this stream and convert the integer values into Character objects. To convert a primitive value to an object - we use the mapToObj() method:

Function<IntStream, Stream<Character>> charF = s -> s.mapToObj(val -> (char) val);

This function transforms an IntStream into a Stream of characters. Finally, we can chain these two, mapping the words in the original stream to a new stream, in which all of the words have passed through these two transformative functions:

words
    // Chaining functions
    .map(intF.andThen(charF))
    // Observe the mapped values
    .forEach(s -> System.out.println(s.collect(Collectors.toList())));

And on running the code snippet, you will get the output:

[l, o, r, e, m]
[i, p, s, u, m]
[d, o, l, o, r]
[s, i, t]
[a, m, e, t]

After collecting the stream into a list - we've ended up with a list of lists. Each list contains the characters of one of the words in the original stream. This isn't a flattened list - it's two dimensional.

If we were to flatten the list - it'd only be one list, containing all of the characters from all of the words sequentially.

This is where flatMap() kicks in.

Instead of chaining these two functions as we have, we can map() the words using intF and then flatMap() them using charF:

List listOfLetters = words
    .map(intF)
    .flatMap(charF)
    .collect(Collectors.toList());

System.out.println(listOfLetters);

Which produces the output:

[l, o, r, e, m, i, p, s, u, m, d, o, l, o, r, s, i, t, a, m, e, t]

As we can see, flatMap() applies a given function to all the available streams before returning an cumulative stream, instead of a list of them. This feature is useful in other implementations too. Similar to the Stream API, Optional objects also offer map() and flatMap() operations.

For instance, the flatMap() method helps in unwrapping Optional objects, such as Optional<Optional<T>>. On unwrapping, such a nested Optional results in Optional<T>.

In this guide we'll explore the use cases of flatMap() and also put them to practice.

Definitions

Let's start off with the definitions and the method's signature:

// Full generics' definition omitted for brevity
<R> Stream<R> flatMap(Function<T, Stream<R>> mapper)

The flatMap() operation returns a cumulative stream, generated from multiple other streams. The elements of the stream are created by applying a mapping function to each element of the constituent streams, and each mapped stream is closed after its own contents have been placed into the cumulative stream.

T represents the class of the objects in the pipeline. R represents the resulting class type of the elements that will be in the new stream. Thus, from our previous example, we can observe how the class types are transforming.

The lambda-bodied Function we've used earlier:

Function<IntStream, Stream<Character>> charF = s -> s.mapToObj(val -> (char) val);

Is equivalent to:

Function charF = new Function<IntStream, Stream<Character>>(){
    @Override
    public Stream<Character> apply(IntStream s){
        return s.mapToObj(val -> (char) val);
    }
};

The charF function accepts an input T of type IntStream. Then, it applies a mapper, which returns a stream containing elements of type R. And, in this case R is Character.

Conditions

The mapper that flatMap() uses should be:

  1. Non-interfering
  2. Stateless

Remember, from what we have seen, the mapper for the charF function is:

s.mapToObj(val -> (char) val);

And, when you expand this mapper into its anonymous class equivalent you get:

new IntFunction<Character>(){
    @override
    public Character apply(int val){
        return (char) val;
    }
};

In terms of non-interference, note how the mapper does not modify the elements in the stream. Instead, it creates new elements from the ones in the stream. It casts each int value in the stream into a char value.

Then the flatMap() operation places those new char values into a new stream. Next, it boxes those char values into their Character wrapper object equivalents. This is the standard practice in all collections too. Primitive values like char and int cannot be used in collections or streams for that matter.

The mapper needs to be stateless also. In simple terms, the mapper function should not depend on the state of the stream that is supplying it with elements. In other terms - for the same input, it should absolutely always give the same output.

In our case, we see that the mapper simply casts all the int values it gets from the stream. It does not interrogate the stream's condition in any way. And, in return, you could be sure that the mapper would return predictable results even in multi-threaded operations.

Using flatMap() to Flatten Streams

Say you want to sum the elements of multiple streams. It would make sense to flatMap() the streams into a single one and then sum all of the elements.

A simple example of a 2D collection of integers is Pascal's Triangle:

[1]
[1, 1]
[1, 2, 1]
...

A triangle like this can work as a simple stub for streams of other data we may encounter. Working with lists of lists isn't uncommon, but is tricky. For instance, lists of lists are oftentimes created when grouping data together.

If you'd like to read more about grouping, read our Guide to Java 8 Collectors: groupingBy()!

Your data could be grouped by a date and represent the pageviews generated by hour, for example:

{1.1.2021. = [42, 21, 23, 52]},
{1.2.2021. = [32, 27, 11, 47]},
...

If you'd like to calculate the sum of these - you could run a loop for each date or stream/list and sum the elements together. However, reduction operations like this are simpler when you have one stream, instead of many - so you could unwrap these into a single stream via flatMap() before summing.

Let's create a Pascal Triangle generator to stub the functionality of an aggregator that aggregates grouped data:

public class PascalsTriangle {
    private final int rows;
    
    // Constructor that takes the number of rows you want the triangle to have
    public PascalsTriangle(int rows){
        this.rows = rows;
    }
    
    // Generates the numbers for every row of the triangle
    // Then, return a list containing a list of numbers for every row
    public List<List<Integer>> generate(){
        List<List<Integer>> t = new ArrayList<>();
        // Outer loop collects the list of numbers for each row
        for (int i = 0; i < rows; i++){
            List<Integer> row = new ArrayList<>();
            // Inner loop calculates the numbers that will fill a given row
            for (int j = 0; j <= i; j++) {
                row.add(
                    (0 < j && j < i)
                    ? (
                        t.get(i - 1).get(j - 1)
                        + t.get(i - 1).get(j)
                    )
                    : 1
                );
            }
            t.add(row);
        }        
        return t;
    }
}

Now, let's generate a 10-row triangle and print the contents:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

PascalsTriangle pt = new PascalsTriangle(10);
List<List<Integer>> vals = pt.generate();
vals.stream().forEach(System.out::println);

This results in:

[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]

We can either flatten the entire list here, and then sum the numbers up or we can sum up the numbers in each list, flatten it and then sum those results.

Code-wise, we can pass in a mapper while flattening a list of streams. Since we're ultimately arriving at an integer, we're flat mapping to an integer. This is a transformative operation and we can define a standalone mapper Function that sums the streams up.

Note: For flat mapping to specific types and using mappers to achieve that - we can use the flatMapToInt(), flatMapToLong() and flatMapToDouble() methods. These were introduced as specialized flat mapping methods to avoid explicit or implicit casting during the process, which can prove costly on larger datasets. Previously, we cast each char to a Character because we didn't use a mapper. If you can use a specialized variant, you're mean to use it.

The mapper defines what happens to each stream before flattening. This makes it shorter and cleaner to define a mapper upfront and just run flatMapToInt() on the summed numbers in the lists, summing them together in the end!

Let's start with creating a mapper. We'll override the apply() method of a Function, so that when we pass it into flatMap() it gets applied to the underlying elements (streams):

Function<List<Integer>, IntStream> mapper = new Function<>() {
    @Override
    public IntStream apply(List<Integer> list){
        return IntStream.of(
                list.stream()
                    .mapToInt(Integer::intValue)
                    .sum()
        );
    }
};  

Or, we could've replaced the entire body with a simple Lambda:

Function<List<Integer>, IntStream> mapper = list -> IntStream.of(
        list.stream()
             .mapToInt(Integer::intValue)
             .sum()
);

The mapper accepts a list of integers and returns a sum of the elements. We can use this mapper with flatMap() as:

int total = vals.stream.flatMapToInt(mapper).sum();
System.out.println(total);

This results in:

1023

Using flatMap() for One-Stream-to-Many Operations

Unlike the map() operation, flatMap() allows you to do multiple transformations to the elements it encounters.

Remember, with map() you can only turn an element of type T into another type R before adding the new element into a stream.

With flatMap(), however, you may turn an element, T, into R and create a stream of Stream<R>.

As we shall see, that capability comes in handy when you want to return multiple values out of a given element back into a stream.

Expand a Stream

Say, you have a stream of numbers:

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6);

And you want to expand that stream in such a way that every number is duplicated. This is, surprisingly enough, dead simple:

Stream<Integer> duplicatedNumbers = numbers.flatMap(val -> Stream.of(val, val));
duplicatedNumbers.forEach(System.out::print);

Here, we flat-mapped the Streams created by each element in the numbers stream, in such a way to contain (val, val). That's it! When we run this code, it results in:

112233445566

Transform a Stream

In some use cases, you may not even want to unwrap a stream fully. You may be only interested in tweaking the contents of a nested stream. Here too, flatMap() excels because it allows you to compose new streams in the manner you desire.

Let's take a case where you want to pair up some elements from one stream with those from another stream. Notation-wise, assume you have a stream containing the elements {j, k, l, m}. And, you want to pair them with each of the elements in the stream, {n, o, p}.

You aim to create a stream of pair lists, such as:

[j, n]
[j, o]
[j, p]
[k, n]
.
.
.
[m, p]

Accordingly, let's create a pairUp() method, that accepts two streams and pairs them up like this:

public Stream<List<?>> pairUp(List<?> l1, List<?> l2){
    return l1.stream().flatMap(
            // Where fromL1 are elements from the first list (l1)
            fromL1 -> {
                return l2.stream().map(
                        // Where fromL2 are elements from the second list (l2)
                        fromL2 -> {
                            return Arrays.asList(
                                    fromL1, fromL2
                            );
                        }
                );
            }
    );
}

The flatMap() operation in this case saves the pairUp() method from having to return Stream<Stream<List<?>>>. This would have been the case if we would have initiated the operation as:

public Stream<Stream<List<?>>> pairUp(){
    return l1.stream.map( ... );
}

Otherwise, let's run the code:

List<?> l1 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<?> l2 = Arrays.asList(7, 8, 9);

Stream<List<?>> pairedNumbers = pairUp(l1, l2);
pairedNumbers.forEach(System.out::println);

We get the output:

[1, 7]
[1, 8]
[1, 9]
[2, 7]
[2, 8]
[2, 9]
[3, 7]
[3, 8]
[3, 9]
[4, 7]
[4, 8]
[4, 9]
[5, 7]
[5, 8]
[5, 9]
[6, 7]
[6, 8]
[6, 9]

Unwrapping Nested Optionals using flatMap()

Optionals are containers for objects, useful for eliminating regular null checks and wrapping empty values in containers we can handle more easily and safely.

If you'd like to read more about Optionals, read our Guide to Optionals in Java 8!

We are interested in this type because it offers the map() and flatMap() operations like the Streams API does. See, there are use cases where you end up with Optional<Optional<T>> results. Such results indicate poor code design, and if you can't employ an alternative - you can eliminate nested Optional objects with flatMap().

Let's create an environment in which you could encounter such a situation. We have a Musician who may produce a music Album. And, that Album may have a CoverArt. Of course, someone (say, a graphic designer) would have designed the CoverArt:

public class Musician {
    private Album album;    
    public Album getAlbum() {
        return album;
    }
}

public class Album {
    private CoverArt art;    
    public CoverArt getCoverArt() {
        return art;
    }
}

public class CoverArt {
    private String designer;    
    public String getDesigner() {
        return designer;
    }
}

In this nested sequence, to get the name of the designer who made the cover art, you could do:

public String getAlbumCoverDesigner(){
    return musician
        .getAlbum()
        .getCoverArt()
        .getDesigner();
}

Yet, code-wise, you are bound to encounter errors if the said Musician has not even released an Album in the first place - a NullPointerException.

Naturally, you can mark these as Optional as they are, in fact optional fields:

public class Musician {
    private Optional<Album> album;
    public Optional<Album> getAlbum() {
        return album;
    }
}

public class Album {
    private Optional<CoverArt> art;
    public Optional<CoverArt> getCoverArt() {
        return art;
    }
}

// CoverArt remains unchanged

Still, when someone asks the question about who a CoverArt designer was, you would continue to encounter errors with your code. See, calling the re-done method, getAlbumCoverDesigner() would still fail:

public Optional<String> getAlbumCoverDesigner(){
    Musician musician = new Musician();
    
    Optional.ofNullable(musician)
        .map(Musician::getAlbum)
        // Won't compile starting from this line!
        .map(Album::getCoverArt)
        .map(CoverArt::getDesigner);
    // ...
}

This is because the lines:

Optional.ofNullable(musician)
        .map(Musician::getAlbum)

Return a type Optional<Optional<Album>>. A correct approach would be to use the flatMap() method instead of map().

public Optional<String> getAlbumCoverDesigner(){
    Musician musician = new Musician();
        
    return Optional.ofNullable(musician)
        .flatMap(Musician::getAlbum)
        .flatMap(Album::getCoverArt)
        .map(CoverArt::getDesigner)
        .orElse("No cover designed");
}

Ultimately, the flatMap() method of Optional unwrapped all the nested Optional statements. Yet, you should also notice how orElse() has contributed to the readability of the code. It helps you to provide a default value in case the mapping comes up empty at any point in the chain.

Conclusion

The Streams API offers several helpful intermediate operations such as map() and flatMap(). And in many cases, the map() method proves sufficient when you need to transform the elements of a stream into another type.

Yet, there are instances when the results of such mapping transformations end up producing streams nested within other streams.

And that could hurt code usability because it only adds an unnecessary layer of complexity.

Fortunately, the flatMap() method is able to combine elements from many streams into the desired stream output. Also, the method gives users the freedom to compose the stream output as they wish. This is contrary to how map() places transformed elements in the same number of streams as found. This means, in terms of stream output, the map operation offers a one-to-one transformation. On the other hand, flatMap() can produce a one-to-many conversion.

The flatMap() method also serves to simplify how the Optional container object works. Whereas the map() method can extract values from an Optional object, it may fail if code design causes the nesting of the optionals. In such cases, flatMap() plays the crucial role of ensuring that no nesting occurs. It transforms objects contained in Optional and returns the result in a single layer of containment.

Find the full code used in this article in this GitHub repository.

Last Updated: May 17th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Hiram KamauAuthor

In addition to catching code errors and going through debugging hell, I also obsess over whether writing in an active voice is truly better than doing it in passive.

Make Clarity from Data - Quickly Learn Data Visualization with Python

Learn the landscape of Data Visualization tools in Python - work with Seaborn, Plotly, and Bokeh, and excel in Matplotlib!

From simple plot types to ridge plots, surface plots and spectrograms - understand your data and learn to draw conclusions from it.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms