In this post, we will be taking a look at the map and flatMap methods introduced in Java 8. We will be looking at a scenario where the map method does not produce the required output and why we really need the flatMap method. I will be using multiple examples to illustrate the same and also show you imperative and declarative styles of coding.
map: Let us consider the scenario where we have a collection of Person objects. Given a list of person objects, what if we wanted to return only the names of the people who have UK citizenship.
The imperative style of writing code would give us the following:
To get this, we have to initialize an empty list, iterate through the loop, filter the people who have UK citizenship and then add the names to the empty list we create.Let’s try and solve this the declarative way, using the map method.
The map method basically takes an object of one type and gives us an object of another type.
The signature of the map method:
The signature looks complicated but it is easy to understand. The R is the output type of the stream and T is the input type.
Remember how the filter method (explained here) took a Predicate as a parameter? The map method takes a Function. This is also a functional interface. The function gets applied to each element. The p -> p.getName is a lambda expression, the function that gets applied to the Person object. To understand this better, we could write the same thing as follows:
Remember that the map method takes one object and returns exactly one object. This is 1-1.
Few more examples of map API:
1.Given a list of numbers, we want to generate the square of each number:
We have input as a list of numbers of 1 type and we want to transform it:
Input: List<Integer> numbers = Arrays.asList(1,2,3,4,5);
[2, 4, 6, 8, 10]
2. Given a list of string, convert each string to upper case:
Transformation of one type to another, this time String to String – use map function
List<String> strings = Arrays.asList(“abc”,”pqr”,”xyz”);
Output: [ABC, PQR, XYZ]
3.Given a list of string, find the length of each string:
List<String> strings = Arrays.asList(“abc”,”pqr”,”xyz”);
Input to the map is a string and output is the length of each string.
Output: [3, 3, 3]
FlatMap: To understand the flatMap, let us consider a different example. Let us consider there are 3 systems and each system returns a list of strings. There is an application which combines these lists and sends the data back to us. Our system needs to take this input and generate all the strings as a single output.
List<String> system1 = Arrays.asList(“AB”,”cd”,”EF”);
List<String> system2 = Arrays.asList(“GH”,”ij”);
List<String> system3 = Arrays.asList(“kl”,”MN”,”op”);
List<List<String>> input = Arrays.asList(system1,system2,system3);
Attempt 1: The input type is a List of List<String>. We want to get all the strings from this input. We know that the map function helps us to transform an object. Will it help us here? Let us take this step by step.
The call to the input.stream() returns a Stream<List<String>>
This gives an output:
[AB, CD, EF]
[kl, MN, op]
When we apply the map function, each time we are getting a list. But we need the individual elements in that list as a combined result. How do we get that?
When we have a single list as shown below and we applied the stream() to method to it, what happened?
List<String> strings = Arrays.asList(“A”,”b”,”C”);
This gave us the individual elements in that stream. So will applying the stream method to the list above solve the issue? Let’s try
Attempt 2: Applying a stream to the list and using a map
This gives a weird output like this:
This gives us a stream of objects. So the usage of the map method in this scenario is not right. This is because the map method as mentioned earlier takes an input and produces one output. But in this case, the map method takes a list and we want the individual elements of that list to be combined together. This is not what the map function does. We need to use a different function.
Attempt 3: Using a flatMap
This gives us the required output:
Let us break flatMap up into 2 operations:
[ [AB, CD, EF] [GH, ij] [kl, MN, op] ]
Stream 1 Stream 2 Stream 3
AB, cd, EF GH, ij kl ,MN ,op
The flatMap() does a map + flat. Flattening here is of the individual streams from each item in the list to a single stream. The flatMap() methods needs a stream and hence the list->list.stream().The flatMap method takes an input and can give either 0 or more elements.
Let us consider another example to understand this well. Let us consider 3 different football leagues. The English Premier league, the LIGA BBVA or the Spanish League and the Bundesliga. Each league has a list of teams. We can represent this as:
Problem: Given a list of leagues, we need to return the names of the all the teams in the leagues.
Declarative style using map:
We have a list of leagues. When we call stream() on it, it will operate on a single League object. But a league has multiple teams in it. If we try solving this using map() then we land up getting this:
We land up getting 3 streams. This means that the map operation is not the right fit here. We need a function that can take these streams and combine each of these elements in these streams to a unified output.
This is the scenario for a flatMap() as we have a collection of collections. So the input is a collection of leagues and each league has another collection which is team.
When you have a collection of collections and want a unified output, use the flatMap.
I hope you have understood the basics of both map and flatMap methods and the scenarios in which they should be applied.