In the following sections, we'll focus on the map and flatMap methods in the Flux class. Those of the same name in the Mono class work just the same way.
2. Maven Dependencies
To write some code examples, we need the Reactor core dependency:
<dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> <version>3.3.9.RELEASE</version> </dependency>
3. The map Operator
Now, let's see how we can use the map operator.
The Flux#map method expects a single Function parameter, which can be as simple as:
Function<String, String> mapper = String::toUpperCase;
This mapper converts a string to its uppercase version. We can apply it on a Flux stream:
Flux<String> inFlux = Flux.just("baeldung", ".", "com"); Flux<String> outFlux = inFlux.map(mapper);
The given mapper converts each item in the input stream to a new item in the output, preserving the order.
Let's prove that:
StepVerifier.create(outFlux) .expectNext("BAELDUNG", ".", "COM") .expectComplete() .verify();
Notice the mapper function isn't executed when the map method is called. Instead, it runs at the time we subscribe to the stream.
4. The flatMap Operator
It's time to move on to the flatMap operator.
4.1. Code Example
Similar to map, the flatMap operator has a single parameter of type Function. However, unlike the function that works with map, the flatMap mapper function transforms an input item into a Publisher rather than an ordinary object.
Here's an example:
Function<String, Publisher<String>> mapper = s -> Flux.just(s.toUpperCase().split(""));
In this case, the mapper function converts a string to its uppercase version, then splits it up into separate characters. Finally, the function builds a new stream from those characters.
We can now pass the given mapper to a flatMap method:
Flux<String> inFlux = Flux.just("baeldung", ".", "com"); Flux<String> outFlux = inFlux.flatMap(mapper);
The flat-mapping operation we've seen creates three new streams out of an upstream with three string items. After that, elements from these three streams are split and intertwined to form another new stream. This final stream contains characters from all three input strings.
We can then subscribe to this newly formed stream to trigger the pipeline and verify the output:
List<String> output = new ArrayList<>(); outFlux.subscribe(output::add); assertThat(output).containsExactlyInAnyOrder("B", "A", "E", "L", "D", "U", "N", "G", ".", "C", "O", "M");
Note that due to the interleaving of items from different sources, their order in the output may differ from what we see in the input.
4.2. Explanation of the Pipeline Operations
We've just gone through defining a mapper, passing it to a flatMap operator, and invoking this operator on a stream. It's time to dive deep into the details and see why items in the output may be out of order.
First, let's be clear that no operations occur until the stream is subscribed. When that happens, the pipeline executes and invokes the mapper function passed to the flatMap method.
At this point, the mapper performs the necessary transformation on elements in the input stream. Each of these elements may be transformed into multiple items, which are then used to create a new stream. In our code example, the value of the expression
Flux.just(s.toUpperCase().split("")) indicates such a stream.
Once a new stream – represented by a Publisher instance – is ready, flatMap eagerly subscribes. The operator doesn't wait for the publisher to finish before moving on to the next stream, meaning the subscription is non-blocking.
Since the pipeline handles all the derived streams simultaneously, their items may come in at any moment. As a result, the original order is lost. If the order of items is important, consider using the flatMapSequential operator instead.
5. Differences Between map and flatMap
So far, we've covered the map and flatMap operators. Let's wrap up with major differences between them.
5.1. One-to-One vs. One-to-Many
The map operator applies a one-to-one transformation to stream elements, while flatMap does one-to-many. This distinction is clear when looking at the method signature:
- <V> Flux<V> map(Function<? super T, ? extends V> mapper) – the mapper converts a single value of type T to a single value of type V
- Flux<R> flatMap(Function<? super T, ? extends Publisher<? extends R>> mapper) – the mapper converts a single value of type T to a Publisher of elements of type R
We can see that in terms of functionality, the difference between map and flatMap in Project Reactor is similar to the difference between map and flatMap in the Java Stream API.
5.2. Synchronous vs. Asynchronous
Here are two extracts from the API specification for the Reactor Core library:
- map: Transform the items emitted by this Flux by applying a synchronous function to each item
- flatMap: Transform the elements emitted by this Flux asynchronously into Publishers
It's easy to see map is a synchronous operator – it's simply a method that converts one value to another. This method executes in the same thread as the caller.
The other statement – flatMap is asynchronous – is not that clear. In fact, the transformation of elements into Publishers can be either synchronous or asynchronous.
In our sample code, that operation is synchronous since we emit elements with the Flux#just method. However, when dealing with a source that introduces high latency, such as a remote server, asynchronous processing is a better option.
The important point is that the pipeline doesn't care which threads the elements come from – it just pays attention to the publishers themselves.
In this article, we've walked through the map and flatMap operators in Project Reactor. We discussed a couple of examples and clarified the process.
As usual, the source code for our application is available over on GitHub.