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:
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:
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.