1. Overview

The Java Stream API introduced many features that significantly enhance the functionality and readability of our code. Among these, the map() method stands out as a powerful tool for transforming elements within a collection. A common requirement is ensuring that the results of these transformations don’t include null elements.

In this tutorial, we’ll explore how to effectively collect non-null elements from Stream‘s map() method.

2. Introduction to the Problem

The map() method provides a high-level abstraction for working with sequences of elements. It’s an intermediate operation that applies a mapper function to each element of the Stream, producing a new Stream of transformed elements.

Sometimes, the mapper function can return null. However, we want to exclude those null values from the transformed results. For example, let’s say we have a List of String values:

static final List<String> INPUT = List.of("f o o", "a,b,c,d", "b a r", "w,x,y,z", "w,o,w");

We’d like to transform the String elements in INPUT using the following mapper function:

String commaToSpace(String input) {
    if (input.contains(",")) {
        return input.replaceAll(",", " ");
    } else {
        return null;
    }
}

As we can see, the commaToSpace() method simply replaces all commas with spaces in the input String and returns the result. However, the method returns null if input doesn’t contain a comma.

Now, we want to use commaToSpace() to transform our INPUT and ensure that null values aren’t included in the result. So, here’s our expected outcome:

static final List<String> EXPECTED = List.of("a b c d", "w x y z", "w o w");

As we can see, the INPUT List has five elements, but the EXPECTED List has three.

It’s worth mentioning that, in practice, we might take a more straightforward approach to this task. For example, we could first filter out String elements that don’t contain any commas and then perform the comma-to-space substitutions. However, since we want to demonstrate how to collect non-null elements from a Stream‘s map() method call, we’ll use the commaToSpace() method as the mapper function and invoke it with Stream.map().

Next, let’s see how to achieve it using Stream API and the map() method.

3. Using the map() + filter() Approach

We’ve mentioned the map() method applies a mapper function, which in this case is commaToSpace(), to each element of the Stream to complete the transformation.

The mapper function takes one input and produces one transformed output, and the map() method doesn’t perform any filtering. Therefore, the Stream that map() produces is always the same size as the original Stream. In other words, if the mapper function returns null, those null values are in the transformed Stream. However, we can use the filter() method in conjunction with the map() method to remove null elements from the resulting Stream.

Next, let’s see how this is done through a test:

List<String> result = INPUT.stream()
  .map(str -> commaToSpace(str))
  .filter(Objects::nonNull)
  .collect(Collectors.toList());
 
assertEquals(EXPECTED, result);

In the above code, we use the filter() method with the Objects::nonNull method reference to remove all null elements from the resulting Stream.

4. How About Using Optional to Handle null Values?

When it comes to handling null values, some may consider leveraging the Optional class, which is designed to handle optional values without explicitly using null:

List<String> result = INPUT.stream()
  .map(str -> Optional.ofNullable(commaToSpace(str)))
  .filter(Optional::isPresent)
  .map(Optional::get)
  .collect(Collectors.toList());
 
assertEquals(EXPECTED, result);

As the above example shows, we first wrap nullable values in Optional objects, resulting in a Stream<Optional>. Then, we remove all absent Optionals from the Stream using filter(). Finally, to obtain the String values held by Stream<Optional>, we need an extra step to extract the value with map(Optional::get).

Therefore, as we can see, the Optional approach isn’t efficient for this problem due to the unnecessary wrapping and unwrapping of elements in the Streams.

5. What if the Mapper Function Returns Optional?

We’ve discussed that using Optional to handle null elements is inefficient for this problem. However, there are cases where the mapper function returns an Optional object instead of a nullable result, for instance:

Optional<String> commaToSpaceOptional(String input) {
    if (input.contains(",")) {
        return Optional.of(input.replaceAll(",", " "));
    } else {
        return Optional.empty();
    }
}

In such situations, we can use Optional.orElse(null) to extract the element value from the Optional returned by the mapper function. *This allows us to convert absent Optionals to null elements within the map() method*:

List<String> result = INPUT.stream()
  .map(str -> commaToSpaceOptional(str).orElse(null))
  .filter(Objects::nonNull)
  .collect(Collectors.toList());
 
assertEquals(EXPECTED, result);

As the code shows, the map() method performs two tasks:

  • transform the Stream using the mapper function
  • unwrap each transformed Optional object

The rest of the steps are the same as in the “*map() + filter()*” approach.

6. Conclusion

In this article, we’ve explored how to collect non-null elements from Stream‘s map() effectively. Additionally, we’ve discussed why wrapping the mapper function’s results in Optionals can lead to inefficiencies.

As always, the complete source code for the examples is available over on GitHub.