Collectors.toMap中的Java 8 NullPointerException

如果其中一个值为“null”,则Java 8 Collectors.toMap将引发NullPointerException 。 我不明白这个行为,地图可以包含空指针作为值没有任何问题。 Collectors.toMap值不能为null是否有充分的理由?

此外,是否有一个很好的Java 8的方式来解决这个问题,或者我应该恢复到普通的旧的循环?

我的问题的一个例子:

 import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; class Answer { private int id; private Boolean answer; Answer() { } Answer(int id, Boolean answer) { this.id = id; this.answer = answer; } public int getId() { return id; } public void setId(int id) { this.id = id; } public Boolean getAnswer() { return answer; } public void setAnswer(Boolean answer) { this.answer = answer; } } public class Main { public static void main(String[] args) { List<Answer> answerList = new ArrayList<>(); answerList.add(new Answer(1, true)); answerList.add(new Answer(2, true)); answerList.add(new Answer(3, null)); Map<Integer, Boolean> answerMap = answerList .stream() .collect(Collectors.toMap(Answer::getId, Answer::getAnswer)); } } 

堆栈跟踪:

 Exception in thread "main" java.lang.NullPointerException at java.util.HashMap.merge(HashMap.java:1216) at java.util.stream.Collectors.lambda$toMap$168(Collectors.java:1320) at java.util.stream.Collectors$$Lambda$5/1528902577.accept(Unknown Source) at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502) 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 Main.main(Main.java:48) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) 

Collectors的静态方法是不可能的。 toMap的javadoc解释了toMap基于Map.merge

@param mergeFunction一个合并函数,用于解决与提供给Map#merge(Object, Object, BiFunction)}键相关的值之间的冲突

Map.merge的javadoc说:

如果指定的键为空,并且此映射不支持空键,或者或remappingFunction null,则抛出NullPointerException

你可以通过使用列表的forEach方法来避免for循环。

 Map<Integer, Boolean> answerMap = new HashMap<>(); answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer())); 

但这并不比旧的方式简单:

 Map<Integer, Boolean> answerMap = new HashMap<>(); for (Answer answer : answerList) { answerMap.put(answer.getId(), answer.getAnswer()); } 

你可以这样做:

 Map<Integer, Boolean> collect = list.stream() .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll); 

这不是很漂亮,但它的工作原理。 结果:

 1: true 2: true 3: null 

( 本教程对我最有帮助。)

我写了一个Collector ,它与默认的java不同,当你null值时不会崩溃:

 public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return Collectors.collectingAndThen( Collectors.toList(), list -> { Map<K, U> result = new HashMap<>(); for (T item : list) { K key = keyMapper.apply(item); if (result.putIfAbsent(key, valueMapper.apply(item)) != null) { throw new IllegalStateException(String.format("Duplicate key %s", key)); } } return result; }); } 

只需将您的Collectors.toMap()调用replace为对此函数的调用即可解决问题。

这里比@EmmanuelTouzery提出的收集器要简单一些。 如果你喜欢,请使用它:

 public static <T, K, U> Collector<T, ?, Map<K, U>> toMapNullFriendly( Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { @SuppressWarnings("unchecked") U none = (U) new Object(); return Collectors.collectingAndThen( Collectors.<T, K, U> toMap(keyMapper, valueMapper.andThen(v -> v == null ? none : v)), map -> { map.replaceAll((k, v) -> v == none ? null : v); return map; }); } 

我们只需用一些自定义对象nonereplacenull ,然后在整理器中进行相反的操作。

是的,来自我的一个迟到的回答,但我认为这可能有助于了解什么是在引擎盖下发生的情况下,任何人都想编码其他Collector逻辑。

我试图通过编写更本土和直接的方法来解决这个问题。 我认为这是尽可能直接的:

 public class LambdaUtilities { /** * In contrast to {@link Collectors#toMap(Function, Function)} the result map * may have null values. */ public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return toMapWithNullValues(keyMapper, valueMapper, HashMap::new); } /** * In contrast to {@link Collectors#toMap(Function, Function, BinaryOperator, Supplier)} * the result map may have null values. */ public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Supplier<Map<K, U>> supplier) { return new Collector<T, M, M>() { @Override public Supplier<M> supplier() { return () -> { @SuppressWarnings("unchecked") M map = (M) supplier.get(); return map; }; } @Override public BiConsumer<M, T> accumulator() { return (map, element) -> { K key = keyMapper.apply(element); if (map.containsKey(key)) { throw new IllegalStateException("Duplicate key " + key); } map.put(key, valueMapper.apply(element)); }; } @Override public BinaryOperator<M> combiner() { return (map1, map2) -> { map1.putAll(map2); return map1; }; } @Override public Function<M, M> finisher() { return Function.identity(); } @Override public Set<Collector.Characteristics> characteristics() { return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); } }; } } 

而使用JUnit和assertj的testing:

  @Test public void testToMapWithNullValues() throws Exception { Map<Integer, Integer> result = Stream.of(1, 2, 3) .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)); assertThat(result) .isExactlyInstanceOf(HashMap.class) .hasSize(3) .containsEntry(1, 1) .containsEntry(2, null) .containsEntry(3, 3); } @Test public void testToMapWithNullValuesWithSupplier() throws Exception { Map<Integer, Integer> result = Stream.of(1, 2, 3) .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null, LinkedHashMap::new)); assertThat(result) .isExactlyInstanceOf(LinkedHashMap.class) .hasSize(3) .containsEntry(1, 1) .containsEntry(2, null) .containsEntry(3, 3); } @Test public void testToMapWithNullValuesDuplicate() throws Exception { assertThatThrownBy(() -> Stream.of(1, 2, 3, 1) .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null))) .isExactlyInstanceOf(IllegalStateException.class) .hasMessage("Duplicate key 1"); } @Test public void testToMapWithNullValuesParallel() throws Exception { Map<Integer, Integer> result = Stream.of(1, 2, 3) .parallel() // this causes .combiner() to be called .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)); assertThat(result) .isExactlyInstanceOf(HashMap.class) .hasSize(3) .containsEntry(1, 1) .containsEntry(2, null) .containsEntry(3, 3); } 

你怎么用它? 那么,就像testing显示一样,使用它来代替toMap() 。 这使调用代码看起来尽可能干净。

根据Stacktrace

 Exception in thread "main" java.lang.NullPointerException at java.util.HashMap.merge(HashMap.java:1216) at java.util.stream.Collectors.lambda$toMap$148(Collectors.java:1320) at java.util.stream.Collectors$$Lambda$5/391359742.accept(Unknown Source) at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502) 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.guice.Main.main(Main.java:28) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) 

当被称为map.merge

  BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction); 

它会作为第一件事null检查

 if (value == null) throw new NullPointerException(); 

我不经常使用Java 8,所以我不知道是否有更好的方法来修复它,但修复它有点困难。

你可以这样做:

使用filter来过滤所有NULL值,并在Javascript代码中检查服务器是否没有发送任何答案,这个ID意味着他没有回复它。

像这样的东西:

 Map<Integer, Boolean> answerMap = answerList .stream() .filter((a) -> a.getAnswer() != null) .collect(Collectors.toMap(Answer::getId, Answer::getAnswer)); 

或者使用peek,它用来改变元素的stream元素。 使用偷看你可以改变答案更可接受的地图,但它意味着编辑你的逻辑了一下。

听起来,如果你想保持当前的devise,你应该避免Collectors.toMap

如果值是一个string,那么这可能工作: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

NullPointerException是迄今为止最常遇到的exception(至less在我的情况下)。 为了避免这种情况,我采取了防御措施,并添加了一堆空检查,最后我得到了臃肿和难看的代码。 Java 8引入了Optional来处理空引用,所以你可以定义可空和不可空的值。

这就是说,我会包装可选容器中的所有可空引用。 我们也不应该破坏向后兼容性。 这是代码。

 class Answer { private int id; private Optional<Boolean> answer; Answer() { } Answer(int id, Boolean answer) { this.id = id; this.answer = Optional.ofNullable(answer); } public int getId() { return id; } public void setId(int id) { this.id = id; } /** * Gets the answer which can be a null value. Use {@link #getAnswerAsOptional()} instead. * * @return the answer which can be a null value */ public Boolean getAnswer() { // What should be the default value? If we return null the callers will be at higher risk of having NPE return answer.orElse(null); } /** * Gets the optional answer. * * @return the answer which is contained in {@code Optional}. */ public Optional<Boolean> getAnswerAsOptional() { return answer; } /** * Gets the answer or the supplied default value. * * @return the answer or the supplied default value. */ public boolean getAnswerOrDefault(boolean defaultValue) { return answer.orElse(defaultValue); } public void setAnswer(Boolean answer) { this.answer = Optional.ofNullable(answer); } } public class Main { public static void main(String[] args) { List<Answer> answerList = new ArrayList<>(); answerList.add(new Answer(1, true)); answerList.add(new Answer(2, true)); answerList.add(new Answer(3, null)); // map with optional answers (ie with null) Map<Integer, Optional<Boolean>> answerMapWithOptionals = answerList.stream() .collect(Collectors.toMap(Answer::getId, Answer::getAnswerAsOptional)); // map in which null values are removed Map<Integer, Boolean> answerMapWithoutNulls = answerList.stream() .filter(a -> a.getAnswerAsOptional().isPresent()) .collect(Collectors.toMap(Answer::getId, Answer::getAnswer)); // map in which null values are treated as false by default Map<Integer, Boolean> answerMapWithDefaults = answerList.stream() .collect(Collectors.toMap(a -> a.getId(), a -> a.getAnswerOrDefault(false))); System.out.println("With Optional: " + answerMapWithOptionals); System.out.println("Without Nulls: " + answerMapWithoutNulls); System.out.println("Wit Defaults: " + answerMapWithDefaults); } } 

保留所有问题IDs小调整

 Map<Integer, Boolean> answerMap = answerList.stream().collect(Collectors.toMap(Answer::getId, a -> Boolean.TRUE.equals(a.getAnswer()))); 

布尔值不应该为null因为布尔值应该有2个值,而不是3个。也许你应该考虑使用另一种方法。 在你的情况下,我认为你不应该使用布尔类,而是使用基本types布尔值。 (了解更多关于差异) 。
由于boolean变得更便宜,所以你的程序可能会更快一些(特别是使用列表的时候)。 如果您尝试将答案设置为null<null> cannot be converted to boolean ),编译也会警告您。 如果你需要Boolean方法,你应该将简单types转换为setAnswerthis.answer = new Boolean(answer) ,如果Boolean值为null,则抛出IllegalArgumentException )。 如果你真的需要使用三值逻辑 ,你可以要求谷歌如何实现它最好的,或者简单地添加第三种方法(如hasAnswer )。 如果你提供了更多的信息,答案应该是什么,以及setAnswer(null)意思,我可能会提供一个更好的方法来帮助你。

正如@Jasper所指出的那样, answer可能是没有答案的,如果是的话,答案将会是0.因为答案会被转换成JSON响应,所以可能会被其他语言读取,并且有些麻烦可以区分falsenull (特别是因为你经常使用这样的代码: if not boolValue then do...而不是if boolValue == false )。 现在,您有两种方法可以解决您的问题:

1:
添加第三个方法isAnswered ,如果问题得到了回答,则返回true, isAnswered返回false。

2:
不要让answer一个布尔,而是使其成为一个String 。 我认为这是一个更好的方法,因为string通常可以为null (另外,不要忘记它包装在可选的string!),我期望答案是一个string包含他的答案。

 class Answer { private int id; private Optional<String> answer; Answer(int id, String answer) { setId(id); setAnswer(answer); } // setter/getter for id public Optional<String> getAnswer() { return answer; } public void setAnswer(String answer) { setAnswer(Optional.ofNullable(answer)); } public void setAnswer(Optional<String> answer) { this.answer = answer; } } 

确保您的JSON API使用Java的Optional类正确运行。

3:
如果JSON响应只知道它是正确的,无效的还是不给的,请使用枚举:

 enum AnswerType { CORRECT, INCORRECT, NOT_GIVEN } 

这种方式需要更多的工作,但最终效果最好(至less根据我的经验)。

  1. 用一看。

您也可以使用for循环并手动创build地图。 Java8的列表还提供了一个forEach方法,这真的很棒(我已经看到它在许多其他语言,他们真的很干净)。

您也可以使用Boolean来执行第2步,将其包含到Optional但这经常将我引入恶意错误。 特别是当其他dynamic语言读取JSON响应时。 其他的开发者偶尔会看一下这些文档,不会注意到3值逻辑,只是简单地检查布尔值是否为真(甚至如果是假的,则更糟糕)。 这通常会产生令人讨厌的错误。 如果这个值是一个枚举(或者至less是一个给定的常量为3的string),那么开发人员需要在一个正确的开关/例子中覆盖所有的三种情况,因为他将仔细看看这些常量的结果。