Stream API's flatMap operation can create a flattened stream of elements from nested collections in a source.
This article assumes some familiarity with Java 8 Stream API. If you need an overview of Stream API, check out the "Introduction to Stream API through map-reduce."
The streams have the map and flatMap intermediate operations for a general-purpose transformation of data. The map operation does a one-to-one conversion of the source stream's elements and returns a new stream of converted elements. The flatMap, on the other hand, does a one-to-many transformation of the source stream's elements and returns a new flattened stream of transformed elements. To comprehend the utility of the flatMap, let's take an example problem in the next section.
Consider a Book class and a Book collection below, possibly loaded from a database:
class Book {
//constructors..
String title;
String authorName;
//..more fields
}
//Book collection. Possibly loaded from a database.
List<Book> books = new ArrayList<>();
The Book class has a few properties, as shown above. Suppose we want to collect a unique set of all author names that start with letter 'J', we can easily do so with streams as follows:
Set<String> nameSet = books.stream()
.map((b) -> b.authorName) //Map to Stream<String>
.filter((name) -> !name.isEmpty() && //Filter names starting with 'J'
name.charAt(0) == 'J')
.collect(Collectors.toSet()); //Collect into a Set
In order to support multiple authors for a book, we have to modify the Book class to have a list of authors. The changed Book class looks as follows:
class Book {
//constructors..
String title;
List<String> authorNames;
//..more fields
}
Getting the unique set of author names as above is not so easy anymore. Remember that the map method can only do a one-to-one transformation. If we use the map method, we will get a stream of name lists, not a stream of names; therefore, doing any further processing of names in the stream pipeline is crafty if not impossible. Nevertheless, let's have a look at a solution by using the map method:
//This set is captured by the forEach lambda.
Set<String> nameSet = new HashSet<>();
books.stream()
.map((b) -> b.authorNames) //Map to Stream<List<String>>
.forEach((nameList) -> { //for each author name list
for(String name : nameList) {
if(!name.isEmpty() && //filter name starting with 'J'
name.charAt(0) == 'J') {
nameSet.add(name); //Add name to Set
}
}
}); //End forEach lambda
As stated above, the map method returns a stream of name lists - Stream<List<String>>
. Then, we use the forEach terminal operation to iterate over the stream of name lists and add matching author names to a set in a lambda expression. We are collecting and filtering the authors' names in a hand-coded lambda expression passed to the forEach terminal operation. This implementation fails to utilize the streams at best. Instead of the map, what we need is a mapping method that can return us a flattened stream of authors' names (a Stream<String>
) from all lists. Precisely, that's what the flatMap does.
Here is how we can use the flatMap along with the filter to get the unique set of author names:
Set<String> nameSet = books.stream()
.flatMap((b) -> b.authorNames.stream()) //Map to Stream<String>
.filter((name) -> !name.isEmpty() && //Filter names starting with 'J'
name.charAt(0) == 'J')
.collect(Collectors.toSet()); //Collect into a Set
Clearly this solution with flatMap is more elegant and utilizes the streams to their fullest. Also, note how close it is to our first solution with one author-name in the Book.
There could be several realistic situations where we need to flatten items in nested collections. For those situations, the flatMap is an essential stream operation for the one-to-many transformation of the elements into a flattened stream.