How to Allow Null Values with Collectors.toMap() in Java


It’s a known bug that null entry values do not work well with Collectors.toMap() in Java.

Suppose we want to convert List<Thing> list to a Map<String, String> newMap. Let’s also say each entry contains a key and value field (both String).

This usage of Collectors.toMap() would lead to a NullPointerException if getValue() ever returns null.

newMap = list
          .stream()
          .collect(
            Collectors.toMap(
              Thing::getKey,
              Thing::getValue
            )
          );

How can we bypass this bug and allow null values in our Map entries?

1. Using custom Collector (allow duplicate keys)

Instead of Collectors.toMap(), we can use the map’s put() function to add key-value entries.

newMap = list
          .stream()
          .collect(
            HashMap::new, 
            (map, elem) -> map.put(
              elem.getKey(), 
              elem.getValue()
            ), 
            HashMap::putAll
          );

This method uses collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner).

Unlike Collectors.toMap(), in the case of duplicate keys, this method will replace the values, whereas the Collectors.toMap() will throw an IllegalStateException.

The above method is stream implementation of the following:

Map<String, String> newMap = new HashMap<>();
list.forEach((elem) -> map.put(elem.getKey(), elem.getValue()));

2. Using custom Collector (reject duplicate keys)

If we don’t want to accept duplicate keys, like with the implementation of Collectors.toMap(), we can create a custom Collector toMapOfNullables().

This function accepts null keys, null values, and throws IllegalStateException with duplicate keys, even when the original key maps to a null value (mappings with null values differ from those with no mapping)

public static <T, K, U> Collector<T, ?, Map<K, U>> toMapWithNullables(
  Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper
) {
  return Collectors.collectingAndThen(
    Collectors.toList(),
    list -> {
      Map<K, U> map = new LinkedHashMap<>();
      list.forEach(item -> {
        K key = keyMapper.apply(item);
        U value = valueMapper.apply(item);
        if (map.containsKey(key)) {
          throw new IllegalStateException(
            String.format(
              "Duplicate key %s (attempted merging values %s and %s)",
              key,
              map.get(key),
              value
            )
          );
        }
        map.put(key, value);
      });
      return map;
    }
  );
}

We can use this function just as we do a normal Collector.

newMap = list
          .stream()
          .collect(
            toMapOfNullables(
              Thing::getKey,
              Thing::getValue
            )
          );

This method uses collect(Collector<? super T,A,R> collector).