Reduce unnecessary “ceremony” with the map method

4 minute read

Recently I was pair-programming with a co-worker who was struggling with understanding the map function of java.util.stream.Stream. The brought back memories of how I learned the meaning and practical application of the map function.

Lists

Let’s have a look at good-old Lists first

In the very old days (pre Java 5)

In the Java 1.2+ days we had lists without generics, no streams API and no for-each loops.

Suppose we have a List of String containing words (words) and we want to convert it to a list containing the lengths of the words. In these early versions of Java the following had to be done:

public static List getWordLengths(List words) {
    List result = new ArrayList();
    for (int i = 0; i < words.size(); i++) {
        String word = (String) words.get(i);
        result.add(word.length());
    }
    return result;
}

or… if you wanted to abstract away from using an integer index and checking the size, you could use an iterator.

public static List getWordLengths(List words) {
    List result = new ArrayList();
    Iterator iterator = words.iterator();
    while (iterator.hasNext()) {
        String word = (String) iterator.next();
        result.add(word.length());
    }
    return result;
}

The nice thing about the second version is that it required a bit less knowledge about the list. You didn’t need consider the size of the list.

At that moment in time, every Java programmer would almost immediately recognize what the code was supposed to do: “transform every item of the list of words into the length of the word

There was just a large portion of ceremony around the actual transformation.

In the less-old days (pre Java 8)

With Java 5, the required ceremony for doing these kind of transformations was greatly reduced.

Instead of using Iterator or index variables we could use the for-each loop construct. And in addition we got generics, which made the casts obsolete (when the words list was defined as List<String>)

So in order to do the same as before, we would now only need this code (since the words was now of typed with generics )

public static List<Integer> getWordLengths(List<String> words) {
    List<Integer> result = new ArrayList<Integer>();
    for (String word : words) {
        result.add(word.length());
    }
    return result;
}

The great benefit was that we nearly didn’t have to think about “how to iterate” over all the items. We could just focus on the transformation and adding the results to a list.

And again… somewhat experienced Java programmers would immediately see what was going on. Every item of the list was being transformed and put into the result list. We just didn’t have a proper name for “transform every item and put into result list”.

Recent days

With Java 8 we got the Streams API and lambdas. This Streams API introduced the concept of “transform every item” by adding the map method.

public static List<Integer> getWordLengths(List<String> words) {
    return words.stream()
            .map(word -> word.length())
            .collect(Collectors.toList());
}

This map basically says: transform every item and collect the results in a new list. The only “unnecessary” ceremony is the .stream() and the .collect(Collectors.toList()).

Current day

With the helper methods introduced in Java 16, it’s even more convenient since the .collect(...) could be replaced with toList in order to reduce some of the “unnecessary” ceremony.

public static List<Integer> getWordLengths(List<String> words) {
    return words.stream()
            .map(word -> word.length())
            .toList();
}

So with this version we don’t need to think about iterating and creating new collections anymore. We could just focus on the transformation. We just map every item that is contained in the list.

Optional

In addition to collections/streams, there is another part of the standard Java APIs where we can use map to prevent unnecessary ceremony… Optional

Suppose we have a variable that might or might not contain a word:

Optional<String> maybeWord = ...

With boilerplate

Suppose we want to get the length of the word, if there is a word in it. A “naive” approach could be to check whether the optional contains a word, and if so… transform the item. E.g. something like this:

if (maybeWord.isPresent()) {
    return Optional.of(maybeWord.get().length());
} else {
    return Optional.empty();
}

What we basically want to do, is transform every item (0 or 1 item) to it’s length. We use some ceremony to determine the content of the Optional and to create a new Optional containing the result.

Without boilerplate

This sounds quite similar to the .map function we saw earlier. We don’t want to mess around we destructuring and assembling some results ourselves, we’d like the map function to take care of that.

Luckily the creators of Java created an equivalent map function on the Optional class, making it a breeze to transform data contained in an Optional.

return maybeWord.map(word -> word.length());

Keep in mind

It is important to keep in mind that the function/lambda, which does the mapping, shouldn’t modify the original object nor the original list that the stream is created for. Ideally the function should be side-effect free.

Other appearances

The map function is something which could be found in all kinds of APIs and other programming languages too.

For example when doing reactive programming with Spring WebFlux you could use the map function to transform a Mono. Or when using Quarkus you could call map on a Uni to get a transformed Uni.

Updated: