Java 8 flatMap or how to « flat mapped » a stream to another stream

Stream.map() and Stream.flatMap() methods are very close methods.
These map a stream to another one by applying the function passed as argument for each element of the initial stream.
But these are not applied in the same context.

map() expects any type in the return type of its function while flatMap() expects a stream of any type.
Which means that the first one can be used in any case (even the case or a stream is returned in the function) while the second one cannot. That’s why when you apply map() instead of flatMap() the compilation may be fine while the actual result is not which we would like.  For example :

List<String> list = new ArrayList<>();
List<Stream<String>> result = list.stream()
                                              .map(s -> Arrays.stream(s.split("")))
                                              .collect(Collectors.toList());

Ooops I would get a List of String and not a List of Stream.

Example where map() makes sense :

Suppose we have a List of Person.
We want to map it to a List of String where each String is the concatenation of the first name and the last name of the Person.
We would use  java.util.Stream.map()  to achieve it as we are in a 1-1 relationship in terms of elements between the input stream and the target stream. We have a  stream of Person and we map each person of this stream to one String. 
Here ‘s an extract of the java.util.Stream.map()  javadoc:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Returns a stream consisting of the results of applying the given function to the elements of this stream.

Here is a sample code that shows map() in action :

Person david = new Person("david", "doe");		
Person kimber = new Person("kimber", "lee");
 
List<Person> persons = Arrays.asList(david, kimber);
 
List<String> values = persons.stream()
	.map(p->p.getFirstName() + p.getLastName())
	.collect(Collectors.toList());

Example where flatMap() makes sense :

Suppose we have a List of Person where Person may have 1 or more addresses.
We want to map it to a List of String where each String is the concatenation of the first name, the last name and the address of the Person. For each Address of a each Person, we want to produce a distinct formatted String.
The map() method is not suitable here.
Give it a go :

Person david = new Person("david", "doe")
	.addAddress(new Address("14 Washington Street", "Washington", "USA"))
	.addAddress(new Address("140 Paris Street", "Paris", " France"));
 
Person kimber = new Person("kimber", "lee")
	.addAddress(new Address("30 Boston Street", "Boston", "USA"))
	.addAddress(new Address("140 London Street", "London", "England"));
 
List<String> values = persons.stream()
					.map(p-> p.getAddresses().stream()
									 .map(a-> p.getFirstName() + " " + p.getLastName() + ", "
									  + "address = " + a.getStreet() + " " + a.getCity() + " " + a.getCountry())					   			
					)
					.collect(Collectors.toList());

The assignment of collect() to the values variable produces the following compilation error :
Type mismatch: cannot convert from List<Stream<String>> to List<String>.
We finish with a List of Stream of String because mapping a stream of A to a function that returns a stream of B results to a stream of stream of B. Which is rather logical.
In our case, we would like flatten this structure to finish with just a stream of String.
To do that we will use  flatMap() by passing a function that returns a stream.
In this way, all individual streams produced by invoking p.getAdresses().stream() will be flatted in a single stream.

Here ‘s an extract of the java.util.Stream.flatMap()  javadoc:

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element.

To keep it in memory, remember that a stream is an internal iteration.
It is designed to replace external iterations : for/while (performed under the hood).
To go one the parallel, in imperative programming style, sometimes we need to use embedded iterations. For instance: for (..){ for(..){ .... }}.
In functional style, the inner loop is done with flatMap().

Here is the version that works with flatMap() :

Person david = new Person("david", "doe")
	.addAddress(new Address("14 Washington Street", "Washington", "USA"))
	.addAddress(new Address("140 Paris Street", "Paris", " France"));
 
Person kimber = new Person("kimber", "lee")
	.addAddress(new Address("30 Boston Street", "Boston", "USA"))
	.addAddress(new Address("140 London Street", "London", "England"));
 
List<String> values = persons.stream()
					.flatMap(p-> p.getAddresses().stream()
									 .map(a-> p.getFirstName() + " " + p.getLastName() + ", "
									  + "address = " + a.getStreet() + " " + a.getCity() + " " + a.getCountry())					   			
					)
					.collect(Collectors.toList());
Ce contenu a été publié dans flatmap, java, java8, lambda. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *