map() and flatMap() APIs stem from functional languages. In Java 8, we can find them in Optional, Stream and in CompletableFuture (although under a slightly different name).
Streams represent a sequence of objects whereas optionals are classes that represent a value that can be present or absent. Among other aggregate operations, we have the map() and flatMap() methods.
Despite the fact that both have the same return types, they are quite different. Let's explain these differences by analyzing some examples of streams and optionals.
2. Map and Flatmap in Optionals
The map() method works well with Optional — if the function returns the exact type we need:
Optional<String> s = Optional.of("test"); assertEquals(Optional.of("TEST"), s.map(String::toUpperCase));
However, in more complex cases we might be given a function that returns an Optional too. In such cases using map() would lead to a nested structure, as the map() implementation does an additional wrapping internally.
Let's see another example to get a better understanding of this situation:
assertEquals(Optional.of(Optional.of("STRING")), Optional .of("string") .map(s -> Optional.of("STRING")));
As we can see, we end up with the nested structure Optional<Optional<String>>. Although it works, it's pretty cumbersome to use and does not provide any additional null safety, so it is better to keep a flat structure.
That's exactly what flatMap() helps us to do:
assertEquals(Optional.of("STRING"), Optional .of("string") .flatMap(s -> Optional.of("STRING")));
3. Map and Flatmap in Streams
Both methods work similarly for Optional.
The map() method wraps the underlying sequence in a Stream instance, whereas the flatMap() method allows avoiding nested Stream<Stream<R>> structure.
Here, map() produces a Stream consisting of the results of applying the toUpperCase() method to the elements of the input Stream:
List<String> myList = Stream.of("a", "b") .map(String::toUpperCase) .collect(Collectors.toList()); assertEquals(asList("A", "B"), myList);
map() works pretty well in such a simple case. But what if we have something more complex, such as a list of lists as an input?
Let's see how it works:
List<List<String>> list = Arrays.asList( Arrays.asList("a"), Arrays.asList("b")); System.out.println(list);
This snippet prints a list of lists [[a], [b]].
Now let's use a flatMap():
System.out.println(list .stream() .flatMap(Collection::stream) .collect(Collectors.toList()));
The result of such a snippet will be flattened to [a, b].
The flatMap() method first flattens the input Stream of Streams to a Stream of Strings (for more about flattening, see this article). Thereafter, it works similarly to the map() method.
Java 8 gives us the opportunity to use the map() and flatMap() methods that originally were used in functional languages.
We can invoke them on Streams and Optionals. These methods help us to get mapped objects by applying the provided mapping function.
As always, the examples in this article are available over on GitHub.