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)
.