This article is about Java 8 stream feature flatMap. If we have a List<List<Object>>
, how can we turn the list of lists into a single list that contains all the elements from the lists in the same streamflow using the features of Java 8?
Introduction
The answer is simply to use flatMap to flatten the lists of elements into one coherent list of the same elements. flatMap
converts lists to streams in a single Stream and then collects the result into a list.
The best explanation will probably show the example of flatMap in action.
Flattening the List with flatMap – Simple Example
Let’s start with a simple example and continue with a more complex case later in the article. Our simple example will have a list of list with String elements. Each inner list contains a list of String elements which is a tuplet denoting at the first position the level of list and in the second position the overall total position of String element.
List<List<String>> nestedList = asList(
asList("1-1", "1-2", "1-3"),
asList("2-4", "2-5", "2-6", "2-7", "2-8"),
asList("3-9", "3-10")
);
We take this simple list above, and we will push it to our simple flattening method flatteningTheListOfLists(List<List<String>> list).
public List<String> flattenListOfLists(List<List<String>> list) {
return list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
As a result, we will get a list with all the String elements in one list.
@Test
public void testFlatteningTheListOfLists_simpleScenario() {
List<String> flattenedList = flattenListOfLists(nestedList);
assertNotNull(flattenedList);
assertEquals(10, flattenedList.size());
for (String element : flattenedList) {
System.out.print(element);
}
}
Output
1-1, 1-2, 1-3, 2-4, 2-5, 2-6, 2-7, 2-8, 3-9, 3-10,
Flattening the List with flatMap – Realistic Scenario
Instead of a simple play with a happy case scenario, hypothetically, let’s have a more advanced system where we will generate the list of objects directly in the stream. We will have a list of employees with their job position. And we would like to collect all their possible career options for employees above junior engineer level into a single list of all career options across all employees through a department. The resulting list will give us a list of possible combinations of employee promotions above the junior level.
Here is an example of Employee class:
public class Employee {
private Position position;
public Employee(Position position) {
this.position = position;
}
public int getPosition() {
return this.position;
}
}
And here is an example of Employee and Position class:
public class Employee {
private final String name;
private final Position position;
public Employee(String name, Position position) {
this.name = name;
this.position = position;
}
public Position getPosition() {
return position;
}
}
public enum Position {
JUNIOR_ENGINEER(1),
SENIOR_ENGINEER(2),
SOFTWARE_ARCHITECT(3),
ENGINEERING_MANAGER(4);
private final int level;
Position(int level) {
this.level = level;
}
public int getLevel() {
return this.level;
}
}
In order to get the list of all possible career options for all employees above junior level we would create for example something like getAllCareerOptionsAboveJunior(List<Employee> employees)
where class employee property is a list of employees of Employee class. getAllCareerOptionsAboveJunior(List<Employee> employees)
method will go through all the employees, filter employees from the list with level above junior level and create a list of possible career options for that specific employee. All the career options should be than pushed to List
and outputted from stream to List
collector.
public List<Position> getAllCareerOptionsAboveJunior(List<Employee> employees) {
return employees.stream()
.filter(employee -> employee.getPosition().getLevel() > Position.JUNIOR_ENGINEER.getLevel())
.map(employee -> createPossibleCareerOptions(employee.getPosition().getLevel())) // This is bad practice and will not work
.collect(Collectors.toList());
}
Example of createPossibleCareerOptions():
public List<Position> createPossibleCareerOptions(int level) {
return Arrays.stream(Position.values())
.filter(position -> position.getLevel() > level)
.collect(Collectors.toList());
}
As we can see above, we mapped an employee to the list of her/his career options. But we want to collect list of all possibilities, not list of lists. Therefore, the code above does not work while stream map
function maps employee
instance into the list, but stream collector can not collect list of position list into the single list.
To make this code work, we need to introduce a consequent function in a stream that will flatten the list’s mapping into one stream. We need to place flatMap
function for flattening the stream of list of positions into single steam positions for our Java stream.
public List<Position> getAllCareerOptionsAboveJunior(List<Employee> employees) {
return employees.stream()
.filter(employee -> employee.getPosition().getLevel() > Position.JUNIOR_ENGINEER.getLevel())
.map(employee -> createPossibleCareerOptions(employee.getPosition().getLevel()))
.flatMap(List::stream) // I need to flatten list streams into one stream
.collect(Collectors.toList());
}
Finally, let’s put all parts together and try the solution on the list of employees.
List<Employee> employees = asList(
new Employee("Mark", Position.JUNIOR_ENGINEER),
new Employee("Thomas", Position.SENIOR_ENGINEER),
new Employee("Janet", Position.SOFTWARE_ARCHITECT),
new Employee("Percy", Position.ENGINEERING_MANAGER)
);
We can test the result with the test written below:
@Test
public void testFlatteningTheListOfLists_realisticScenario() {
List<Position> allPossiblePromotions = getAllCareerOptionsAboveJunior(employees);
assertNotNull(allPossiblePromotions);
assertEquals(3, allPossiblePromotions.size());
for (Position position : allPossiblePromotions) {
System.out.print(position + ", ");
}
}
Output
SOFTWARE_ARCHITECT, ENGINEERING_MANAGER, ENGINEERING_MANAGER,
Note : If you have better idea for Map
sorting, let me know in comments below.
Flattening the List With forEach
To flatten this nested collection into a list of the same elements, we can also use forEach together with a Java 8 method reference. However, the solution with forEach might be discouraged by some developers as it needs to create a new reference on the list with results before it returns the complete results. Thus, creating the result variable will make the code more cumbersome and brittle. And while it might be suitable for small lists and simple applications, it brings pointlessly more complexity with its wrapping code for more complex scenarios as described above. On the contrary, it provides no advantage in the form of, for example, code readability.
Neither to say, let’s take a look at the option of flattening the stream of element’s list with the help of forEach method.
public List<String> useOfForEach(Stream<List<String>> streamOfLists) {
List<String> result = new ArrayList<>();
streamOfLists.forEach(result::addAll);
return result;
}
If we use the same list of simple String elements as for happy case scenario with flatMap we will get the same results.
Conclusion
We have seen in this article how to use a Java 8 Stream method flatMap first on a simple example and then on a more realistic scenario. We have also seen the option of flattening the stream with forEach. However, use of forEach might be discouraged on more realistic scenario when used on stream of Objects which generates the lists.
Did you find use of flatMap hard? Do you have your trick or you know another way how to use flatMap? Let us know in the comments below the article. We would like to hear your ideas and stories.
As always, you can find all our examples on our GitHub project!