Java - Rewriting Traditional Codes With Streams #2 (map, flatMap)
We are continuing with the new part of the series. In this part, we will explore mapping operations using map and flatMap functions.
We will use the following 2 models in the examples.
// lombok annotations used
@Getter
@Setter
public class Team {
private String name;
private List<Player> players;
public Team(String name) {
this.name = name;
this.players = new ArrayList<>();
}
public void addPlayer(Player player) {
players.add(player);
}
}@Data
@AllArgsConstructor
public class Player {
private String name;
private int age;
}
Converting object to another type: map
When we are processing object, we may need to convert it to another object of different type using functions or acquire different kind of object using it like getting nested object in it. Let’s take a look at a concrete example to understand this better. Consider the following example where we try to print the name of the teams starting with given prefix.
This is how we do it traditional way
private static void printTeamNamesStartingWithTraditional(List<Team> teams, String prefix) {
for (Team team : teams) {
String teamName = team.getName();
if (teamName.startsWith(prefix)) {
System.out.println(team.getName());
}
}
}
To print the name of the team, we are first getting it from team object as String. Then, it will be our main object to achieve our goal. Mapping does not sound very different concept in traditional Java. It is simply creating object from other objects. Let’s look at streams.
For the streams, mapping method simple converts stream type to another type. An example conversion will be something like
Stream<Team> --> Stream<String>
Stream keeps flowing with the return type of the map operation.
private static void printTeamNamesStartingWith(List<Team> teams, String prefix) {
teams
.stream()
.map(Team::getName) // method reference
.filter(name -> name.startsWith(prefix))
.forEach(System.out::println);
}
You can also use built-in mapToInt, mapToLong, mapToDouble functions to map to IntStream, LongStream, DoubleStream.
There is another important mapping method called flatMap. With the help of this method, we can convert a stream of a type to a stream of another type through streamable object (list, set etc) like converting team stream to team’s players stream. It basically expects you to provide a stream as a parameter.
See the example below where we have a list of teams and want to find players aged greater than 20. We first iterate through list of teams and then, for each team, we are iterating over list of player.
private static void printPlayersAgedGreaterThan20Conventional(List<Team> teams) {
for (Team team : teams) {
for (Player player : team.getPlayers()) {
if (player.getAge() > 20) {
System.out.println(player);
}
}
}
}
How can we achieve this using stream? Here, flatMap comes into stage and helps create player stream from team stream. Lambda given to flatMap returns a stream and in our case, stream of type “player”. Then, we can apply other stream operations on “player” stream.
private static void printPlayersAgedGreaterThan20(List<Team> teams) {
teams
.stream()
.flatMap(team -> team.getPlayers().stream())
.filter(player -> player.getAge() > 20)
.forEach(System.out::println);
}
It may sound confusing the first time you see this. To remove confusion, we can compare map and flatMap using example above. Let’s use map instead of flatMap above. The map function call returns a stream of type “List<Player>” not stream of players collected from each team. So with map, we are having a single List<Player> element from each team. This way, we cannot directly use filter or other stream operations on players and a bad practice comes out as follows.
// BAD
teams
.stream()
.map(team -> team.getPlayers())
.forEach(players -> players.stream().filter(player -> player.getAge() > 20).forEach(System.out::println));
So, do not do this undesired behaviour and use flatMap when you need to keep stream flowing through nested streamable fields.
You can also see the diagram below. Notice the map and flatMap operations’ output.
You can also use built-in flatMapToInt, flatMapToLong, flatMapToDouble functions to flatMap to IntStream, LongStream, DoubleStream.
Let’s have some traditional functions and then transform them using streams.
1- Find cartesian product of two list ( which is A × B = {(x, y) : x ∈ A, y ∈ B})
Let’s first code it traditional way. It is a quite simple method. First, we are iterating over the first list and then for each element of the first list, we are iterating over the second list and print each pair.
private static void printCartesianProductTraditional(List<String> list1, List<String> list2) {
for (String str1 : list1) {
for (String str2 : list2) {
System.out.println(str1 + " " + str2);
}
}
}
Now, let’s do this with using map and flatMap.
private static void printCartesianProduct(List<String> list1, List<String> list2) {
list1.stream()
.flatMap(e1 -> list2.stream().map(e2 -> new String[]{e1, e2}))
.forEach(arr -> System.out.println(arr[0] + " " + arr[1]));
}
We are starting with flatMap on the first list and for each element of the first list, we are creating a pair stream (stream of String[]) using map method like (1,A), (1,B) where 1 belongs to first list and second elements belong to second list. In forEach, we are simply printing them. Stream inputted to forEach will be of type Stream<String[]>.
Consider this part as inner loop in the first example.
list2.stream().map(e2 -> new String[]{e1, e2})
See the example below to make the example clearer.
List1: [1,2,3]
List2: [A,B]
FlatMap over list1 and create a pair stream for element 1 : (1,A), (1,B)
continue with element 2 and keep stream flowing with (2,A), (2,B),
finally with element 3 and finish stream with (3,A), (3,B).
Terminal operation forEach will print
(1,A), (1,B), (2,A), (2,B), (3,A), (3,B)
2- Print square of odd elements in the given list of list.
The traditional version will be as follows
private static void printSquareOfOddElementsInTheGivenListOfListsTraditional(List<List<Integer>> list) {
for (List<Integer> l : list) {
for (Integer i : l) {
if (i % 2 != 0) {
System.out.println(i * i);
}
}
}
}
Create nested loop to get elements of the nested lists and print square of them if they are odd.
For the stream version, we are first flattening nested list and filtering over all elements to exclude even numbers and then mapping them to their squares and finally print.
private static void printSquareOfOddElementsInTheGivenListOfLists(List<List<Integer>> lists) {
lists.stream()
.flatMap(Collection::stream)
.filter(i -> i % 2 != 0)
.map(i -> i * i)
.forEach(System.out::println);
}
In this article, I talked about mapping functions of stream api and presented some examples. I hope you liked it. See you on next parts where we see other stream operations.