Take a step back in history with the archives of PragPub magazine. The Pragmatic Programmers hope you’ll find that learning about the past can help you make better decisions for the future.

FROM THE ARCHIVES OF PRAGPUB MAGAZINE NOVEMBER 2017

Refactoring to Functional Style in Java 8: Mapping One-to-Many Relationships

By Venkat Subramaniam

PragPub
The Pragmatic Programmers
7 min readAug 30, 2022

--

All this year, Venkat is exploring how to exploit the benefits of functional style in Java. In this installment of his series, he looks at the intricacies of
one-to-many relationships.

https://pragprog.com/newsletter/
https://pragprog.com/newsletter/

The stream API makes processing collections of data quite easy. As we learn and adapt the functional style of programming in Java, we begin to use functions like filter and map readily and often. However, when the transformation becomes a bit more complex, we have to reach for other methods. In this article we will look at a way to deal with one-to-many transformation of objects in a collection.

Traditional Code to Process a Collection of Objects

Suppose we have a Person class that holds a name of a person and their contact email addresses, like so:

import java.util.*;
public class Person {
private String name;
private List<String> emailAddresses;
public Person(String fullName, String… contactEmailAddresses) {
name = fullName;
emailAddresses = Arrays.asList(contactEmailAddresses);
}
public String getName() { return name; }
public String getEmailAddress() {
return emailAddresses.get(0);
}
public List<String> getEmailAddresses()
{ return emailAddresses; }
}

The class returns the first email address, which we assume is present, for the call to the getEmailAddress() method. It also returns all the email addresses for that person for the call to getEmailAddresses().

Let’s say we have the following sample list of Person instances that we’d like to process:

List<Person> people = Arrays.asList(
new Person(“John Doe”, “john@example.com”, “does@example.com”),
new Person(“Jane Doe”, “jane@example.com”, “does@example.com”));

If we want to get a sorted list of all email addresses of everyone in our collection, removing duplicates, we may write the following traditional, imperative style, code to accomplish that task.

List<String> allEmailAddresses = new ArrayList<>();
for(Person person : people) {
for(String emailAddress : person.getEmailAddresses()) {
if(!allEmailAddresses.contains(emailAddress))
allEmailAddresses.add(emailAddress);
}
}
Collections.sort(allEmailAddresses);
System.out.println(allEmailAddresses);

The code is fairly straightforward and easy to understand for most Java programmers since we’re all used to the imperative style of programming. We iterate over the people collection, getting each person. For each person, we iterate over their email addresses, adding each address to the result collection if the address is not already in it.

If we want to write this code in functional style, how to do this may not be readily obvious for developers learning the ropes of functional style of programming.

So before we jump into a solution, let’s step back and consider a simpler problem first, and then revisit the problem at hand.

A One-to-One Transformation

Let’s take a simpler problem: to get a distinct sorted collection of only primary email addresses. Here’s traditional imperative code for that.

List<String> primaryEmailAddresses = new ArrayList<>();
for(Person person : people) {
String primaryEmailAddress = person.getEmailAddress();
if(!primaryEmailAddresses.contains(primaryEmailAddress))
primaryEmailAddresses.add(primaryEmailAddress);
}

For each person in the collection we get their primary email address and add it to the result collection if it is not already present. Finally, we sort the result collection.

Overall, we’re transforming each person to their primary email address and producing a sorted collection of distinct elements. The transformation or mapping is one-to-one — one email address from each person.

The map function is the obvious choice for this in the functional style. Let’s rework the code to use map to transform the persons and the distinct and sorted methods to remove duplication and sort, respectively.

List<String> primaryEmailAddresses = 
people.stream()
.map(person -> person.getEmailAddress())
.distinct()
.sorted()
.collect(toList());

The toList() method comes from the java.util.stream.Collectors helper class. The people collection is transformed into a collection of primary email addresses, using map of course, and any duplicates are removed from the result, and finally sorted.

That was easy, but one may wonder if we couldn’t just use map for the original problem. After all, that’s a transformation operation, too.

A One-to-Many Transformation

The example we started with wants all the email addresses, not just the primary, for each person. That’s a one-to-many transformation: one person to zero or more email addresses. What if we used map for this too?

Let’s not worry about distinct and sorted right now. Let’s use map and print the result to see what we get.

System.out.println( 
people.stream()
.map(person -> person.getEmailAddresses())
.collect(toList()));

In the function, lambda expression, that’s passed to the map method we transform the Person instance at hand to a list of their contact email addresses. Unlike the previous example, where we transformed a person to one email address, a String, here we have a List<String>. The result, a list of lists of email addresses:

[[john@example.com, does@example.com], 
[jane@example.com, does@example.com]]

The function we pass to map always takes as input a value in the collection being iterated. In this example, it has to take as input a Person instance. The output produced by the lambda expression or function passed to map determines the type of Stream that results from map.

If the function passed to map, called on a Stream<T>, is of the form T -> U, then the result of map is Stream<U>. So, when we used person -> person.getEmailAddress() for the lambda expression, we went from Stream<Person> to Stream<String>.

If the function passed to map, called on a Stream<T> is of the form T -> List<U>, then the result of map is Stream<List<U>>. So, when we used person -> person.getEmailAddresses() for the lambda expression, we went from Stream<Person> to Stream<List<String>>. So, the toList() method faithfully produced a List<List<String>>.

Not quite what we wanted.

Flattening Operation

Many programmers have been stumped by this problem, and one reason may be that Java does not have a flatten function. Java programmers have not been exposed to this idea of easily flattening a list. Let’s explore this function, which is available in many other languages, including Ruby and Groovy.

Here’s a small piece of Groovy code:

list = [1, 2, [3, 4, 5, 6], 7, [8, 9, 10]]
println list.size()

We have a list with a mixture of element types, some integers and some list of integers. The call to the size() method reports that the list has 5 elements.

What if we don’t want a list of mixed elements but only a list of the integers (or objects). That’s where the flatten method comes.

Here’s again a piece of Groovy code that uses flatten on the list:

println list.flatten().size()

The flatten() method, which Groovy adds to Java List, returns a new list of elements, combining or flattening the sublists into one single list. The call to the size() method on the list returned by flatten() reports that there are 10 elements.

We saw that if we have a list of lists, we can flatten it to create a single list in languages like Groovy. Java does have a flatten method on List but streams are special.

Map and then Flatten

When we used map with the lambda expression person -> person.getEmailAddresses(), we received a Stream<List<String>> but we really want a Stream<String>. We know that List does not have flatten(), but imagine for a minute that Stream does. Then, if we have a Stream<Stream<String>> we could use that hypothetical flatten() method to get a Stream<String>. To get a Stream<Stream<String>>, instead of Stream<List<String>>, we can pass to map the lambda expression person -> person.getEmailAddresses().stream() instead of person -> person.getEmailAddresses(). In other words, we can get a Stream from the list returned by getEmailAddresses(). Sounds good.

But there’s a catch.

Java does not have a flatten method on Stream either. “But, you said Streams are special” — you may protest. Yes, they are — instead of performing a map operation first and then performing a flattening, we can do, in one shot, a map-and-flatten operation. While our intent is to perform a mapFlatten operation, flatMap is easier to say and so that’s what the mapFlatten operation is called. Let’s use the flatMap operation to rework our first example.

List<String> allEmailAddresses = 
people.stream()
.flatMap(person -> person.getEmailAddresses().stream())
.distinct()
.sorted()
.collect(toList());

This code is not too different from the example where we used map, distinct, and sort. Line by line, all things are the same in the functional pipeline except for two things. We replaced map with flatMap and we return a Stream instead of List from within the lambda expression passed to the map — err, flatMap — method.

The code is in functional style: concise, expressive, and pretty elegant too, thanks to flatMap.

Conclusion

To transform a collection to another collection of objects, use map if the function at hand transforms an object to another object. On the other hand, if the intent is still the same at the collection level, but the function at hand transforms an object to a collection, then use flatMap.

About Venkat Subramaniam

Dr. Venkat Subramaniam is an award-winning author, founder of Agile Developer, Inc., and an instructional professor at the University of Houston. He has trained and mentored thousands of software developers in the U.S., Canada, Europe, and Asia, and is a regularly invited speaker at several international conferences. Venkat helps his clients effectively apply and succeed with agile practices on their software projects. Venkat is a (co)author of multiple books, including the 2007 Jolt Productivity award-winning book Practices of an Agile Developer.

Cover from PragPub Magazine, November 2017 featuring an illustration of a tree with a lantern hanging from the branch in sepia tones
Cover from PragPub Magazine, November 2017

--

--

PragPub
The Pragmatic Programmers

The Pragmatic Programmers bring you archives from PragPub, a magazine on web and mobile development (by editor Michael Swaine, of Dr. Dobb’s Journal fame).