How to handle NullPointerException in Collectors.toMap when a mapped value is null
In Java 8, Collectors.toMap
throws NullPointerException
when one of the mapped value is null. This is a known bug in OpenJDK. There are many workaround to handle this.
The ProblemPermalink
The problem statement is simple, I have a list of Employee, given that employee’s salary can be null, I want to map of salary by employeeId.
public class CollectorIssue { | |
public static void main(String[] args) { | |
Employee employee1 = new Employee(1, 1000); | |
Employee employee2 = new Employee(2, 2000); | |
Employee employee3 = new Employee(3, null); | |
final List<Employee> employees = new ArrayList<>(); | |
employees.add(employee1); | |
employees.add(employee2); | |
employees.add(employee3); | |
final Map<Integer, Integer> employeeSalaryMap = employees.stream() | |
.collect(Collectors.toMap(Employee::getEmployeeId, Employee::getSalary)); | |
System.out.println(employeeSalaryMap.size()); | |
} | |
@Data | |
static class Employee { | |
private final Integer employeeId; | |
private final Integer salary; | |
} | |
} |
When we run this code, we get a null pointer exception.
Exception in thread "main" java.lang.NullPointerException | |
at java.util.HashMap.merge(HashMap.java:1225) | |
at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320) | |
at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169) | |
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384) | |
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482) | |
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472) | |
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) | |
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) | |
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) | |
at com.cobalt.dap.creative.lego.payload.generator.GeoTargetGenerator.main(GeoTargetGenerator.java:148) |
Solution - Using Custom CollectorPermalink
We are going to create a custom collector, if you have no idea how to create one, follow this post
static class CustomCollector { | |
public static <T, K, V> Collector<T, Map<K, V>, Map<K, V>> toMap(final Function<? super T, K> keyMapper, final Function<T, V> valueMapper) { | |
return Collector.of( | |
HashMap::new, | |
(kvMap, t) -> { | |
kvMap.put(keyMapper.apply(t), valueMapper.apply(t)); | |
}, | |
(kvMap, kvMap2) -> { | |
kvMap.putAll(kvMap2); | |
return kvMap; | |
}, | |
Function.identity(), | |
Collector.Characteristics.IDENTITY_FINISH); | |
} | |
} |
So here is our modified code, we have just replaced Collectors
implementation with CustomCollector
.
public class CollectorIssueSolved { | |
public static void main(String[] args) { | |
Employee employee1 = new Employee(1, 1000); | |
Employee employee2 = new Employee(2, 2000); | |
Employee employee3 = new Employee(3, null); | |
final List<Employee> employees = new ArrayList<>(); | |
employees.add(employee1); | |
employees.add(employee2); | |
employees.add(employee3); | |
final Map<Integer, Integer> employeeSalaryMap = employees.stream() | |
.collect(CustomCollector.toMap(Employee::getEmployeeId, Employee::getSalary)); | |
System.out.println(employeeSalaryMap.size()); | |
} | |
@Data | |
static class Employee { | |
private final Integer employeeId; | |
private final Integer salary; | |
} | |
static class CustomCollector { | |
public static <T, K, V> Collector<T, Map<K, V>, Map<K, V>> toMap(final Function<? super T, K> keyMapper, final Function<T, V> valueMapper) { | |
return Collector.of( | |
HashMap::new, | |
(kvMap, t) -> { | |
kvMap.put(keyMapper.apply(t), valueMapper.apply(t)); | |
}, | |
(kvMap, kvMap2) -> { | |
kvMap.putAll(kvMap2); | |
return kvMap; | |
}, | |
Function.identity(), | |
Collector.Characteristics.IDENTITY_FINISH); | |
} | |
} | |
} |
We have fixed the issue with Collectors.toMap
with our custom implementation.
If you liked this article, you can buy me a coffee
Leave a comment