The map Operator
The map
operator transforms the elements emitted by a Publisher
by applying a function to each of them.
Here’s the marble diagram of this operator (for Mono):
- First, a
Publisher
of circles emits a circle. - Then, the circle is transformed into a square by the operation passed to the
map
operator. - Finally, the square is put into a new
Publisher
(of squares) so it can be processed by another operator or sent to a subscriber.
Thinking in terms of containers, if you have a container of circles:
The map
operator unpacks the circle:
Converts it into a square:
And packs the square in a new container:
Here’s the definition of map
for Mono<T>
and Flux<T>
:
// For Mono
public final <R> Mono<R> map(
Function<? super T,? extends R> mapper
)
// For Flux
public final <V> Flux<V> map(
Function<? super T,? extends V> mapper
)
For example, if we have a Mono
of String
that we want to convert into a Mono
of LocalDate
:
T
would beString
R
(V
in the case ofFlux
) would beLocalDate
We’ll need a Function
of type Function<String, LocalDate>
to make the conversion. For example:
Function<String, LocalDate> stringToDateFunction = s ->
LocalDate.parse(
s,
DateTimeFormatter.ofPattern(
"yyyy-MM-dd",
Locale.ENGLISH
)
);
This way, we can use the function with the map
operator to transform a Mono<String>
to a Mono<LocalDate>
:
Mono<String> monoString = Mono.just("2022-01-01");
Mono<LocalDate> monoDate = monoString.map(stringToDateFunction);
monoDate.subscribe(d ->
System.out.println(
d.format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
)
)
);
For Flux
, we can use the same function, there are no differences:
Flux<String> fluxString = Flux.just(
"2022-01-02", "2022-01-03", "2022-01-04"
);
Flux<LocalDate> fluxDate = fluxString.map(stringToDateFunction);
fluxDate.subscribe(d ->
System.out.println(
d.format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
)
)
);
Notice two things.
One. map
passes the emitted value to the function, it doesn’t pass the whole Mono
or Flux
object.
So, since we’re not required to work with asynchronous types, just with plain types, it is said that map
works with synchronous functions. You don’t need to perform asynchronous operations inside these functions.
This doesn’t mean that you can perform blocking operations (like a database query) inside these functions. Well, technically you can, but this defeats the purpose of reactive programming.
However, you can use imperative programming inside the function if you want:
Function<String, Date> stringToDateImperativeFunction = s -> {
if(s != null && s.contains("-")) {
return LocalDate.parse(
s,
DateTimeFormatter.ofPattern(
"yyyy-MM-dd",
Locale.ENGLISH
)
);
} else {
return LocalDate.now();
}
};
Mono<String> monoString = Mono.just("2022-01-05");
Mono<LocalDate> monoDate = monoString.map(stringToDateImperativeFunction);
monoDate.subscribe(d -> System.out.println(
d.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG))));
Simple logic like the above can be replaced by operators that we’ll review later, but, for example, if your logic involves complex branching or loops and you think imperative programming adds more clarity to the code, you can use it without problems.
And two. The values returned by the map
function will be wrapped inside a new Publisher
.
If the Function
returns an Integer
, you’ll have a Mono
or Flux
of type Integer
:
Mono<Integer> monoInteger = Mono.just(1)
.map(i -> i * 2);
monoInteger.subscribe(System.out::println);
If the Function
returns a list of Integer
, you’ll have a Mono
or Flux
of type List<Integer>
:
Mono<List<Integer>> monoListInteger = Mono.just(1)
.map(i -> Arrays.asList(1));
monoListInteger.subscribe(System.out::println);
See the problem?
You’ll have a container inside another container.
This can be a problem because sometimes you’ll have methods that will return container types such as List
or even asynchronous types like Mono
and Flux
, and then you’ll want to perform another operation on the value, not on the container (Mono
or Flux
):
Mono.just(1)
.map(i -> asyncTransformation(i))
.map(j -> j * 10) // Compiler error,
// the type of j is Mono<Integer>
;
// ...
Mono<Integer> asyncTransformation(int i) {
// Modify i in some way
return Mono.just(i);
}
In the above example, the first map
operator will put in a Mono
the returned value of the asyncTransformation(int)
method. This way, you’ll have a Mono
inside another Mono
, a Mono<Mono<Integer>>
.
The second map
operator will unpack the value of that Mono
so the function it takes can receive an argument of type Mono<Integer>
, not Integer
.
To solve this problem, we have the flatMap
operator.