具有空条目值的 Collectors.toMap 中的 NullPointerException
Posted
技术标签:
【中文标题】具有空条目值的 Collectors.toMap 中的 NullPointerException【英文标题】:NullPointerException in Collectors.toMap with null entry values 【发布时间】:2014-08-29 03:18:39 【问题描述】:如果其中一个值为null
,Collectors.toMap
将抛出NullPointerException
。我不明白这种行为,地图可以包含空指针作为值而没有任何问题。 Collectors.toMap
的值不能为空是否有充分的理由?
另外,有没有很好的 Java 8 方法来解决这个问题,或者我应该恢复到普通的旧 for 循环?
我的问题的一个例子:
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)
这个问题在 Java 11 中仍然存在。
【问题讨论】:
null
总是有点问题,就像在 TreeMap 中一样。也许是试用Optional<Boolean>
的好时机?否则拆分并使用过滤器。
@JoopEggen null
可能是键的问题,但在这种情况下,它是值。
并非所有的地图都存在null
的问题,HashMap
例如可以有一个null
键和任意数量的null
值,您可以尝试使用创建自定义Collector
HashMap
而不是使用默认的。
@kajacx 但默认实现是HashMap
- 如stacktrace的第一行所示。问题不在于Map
不能保存null
值,而是Map#merge
函数的第二个参数不能为null。
就个人而言,在给定的情况下,我会使用非流解决方案,或者如果输入是并行的,则使用 forEach()。下面基于短流的不错的解决方案可能会有糟糕的表现。
【参考方案1】:
我稍微修改了Emmanuel Touzery's null-safe map Collector
implementation。
这个版本:
允许空键 允许空值 检测重复键(即使它们为空)并抛出IllegalStateException
,就像在原始 JDK 实现中一样
当键已经映射到空值时,也会检测重复键。换句话说,将具有空值的映射与无映射分开
public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(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;
);
单元测试:
@Test
public void toMapOfNullables_WhenHasNullKey()
assertEquals(singletonMap(null, "value"),
Stream.of("ignored").collect(Utils.toMapOfNullables(i -> null, i -> "value"))
);
@Test
public void toMapOfNullables_WhenHasNullValue()
assertEquals(singletonMap("key", null),
Stream.of("ignored").collect(Utils.toMapOfNullables(i -> "key", i -> null))
);
@Test
public void toMapOfNullables_WhenHasDuplicateNullKeys()
assertThrows(new IllegalStateException("Duplicate key null"),
() -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> null, i -> i))
);
@Test
public void toMapOfNullables_WhenHasDuplicateKeys_NoneHasNullValue()
assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
() -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
);
@Test
public void toMapOfNullables_WhenHasDuplicateKeys_OneHasNullValue()
assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
() -> Stream.of(1, null, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
);
@Test
public void toMapOfNullables_WhenHasDuplicateKeys_AllHasNullValue()
assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
() -> Stream.of(null, null, null).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
);
【讨论】:
对重复消息的轻微改进以反映 Java 11 中的消息,其中还包括重复的值: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);
【参考方案2】:
您可以在 OpenJDK 中解决这个 known bug 问题:
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
(this 教程对我帮助最大。)
编辑:
与Collectors.toMap
不同,如果您多次拥有相同的键,这将默默地替换值,正如@mmdemirbas 在 cmets 中指出的那样。如果您不想要这个,请查看评论中的链接。
【讨论】:
@Jagger 是的,供应商的定义(第一个参数)是一个不传递参数并返回结果的函数,因此您的案例的 lambda 将是() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
以创建不区分大小写的String
键入TreeMap
。
这是正确的答案,恕我直言,JDK 应该为其默认的非重载版本做些什么。也许合并更快,但我没有测试过。
我必须指定类型参数才能编译,这样:Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);
。我有:incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map<java.lang.Integer,java.lang.Boolean>,java.util.Map<java.lang.Integer,java.lang.Boolean>) method java.util.Map.putAll(java.util.Map) is not applicable (actual and formal argument lists differ in length)
在大输入时这可能会很慢。您创建一个HashMap
,然后为每个条目调用putAll()
。就个人而言,在特定情况下,我会使用非流解决方案,如果输入是并行的,我会使用 forEach()
。
请注意,此解决方案的行为与原始 toMap 实现不同。原始实现检测到重复键并抛出 IllegalStatException,但此解决方案静默接受最新的键。 Emmanuel Touzery 的解决方案 (***.com/a/32648397/471214) 更接近原始行为。【参考方案3】:
为了完整起见,我发布了一个带有 mergeFunction 参数的 toMapOfNullables 版本:
public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
return Collectors.collectingAndThen(Collectors.toList(), list ->
Map<K, U> result = new HashMap<>();
for(T item : list)
K key = keyMapper.apply(item);
U newValue = valueMapper.apply(item);
U value = result.containsKey(key) ? mergeFunction.apply(result.get(key), newValue) : newValue;
result.put(key, value);
return result;
);
【讨论】:
【参考方案4】:是的,我的回答迟了,但我认为这可能有助于了解幕后发生的事情,以防有人想编写其他 Collector
-logic 代码。
我尝试通过编写更原生、更直接的方法来解决问题。我认为它尽可能直接:
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 (left, right) ->
int total = left.size() + right.size();
left.putAll(right);
if (left.size() < total)
throw new IllegalStateException("Duplicate key(s)");
return left;
;
@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 的测试:
@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);
@Test
public void testToMapWithNullValuesParallelWithDuplicates() throws Exception
assertThatThrownBy(() -> Stream.of(1, 2, 3, 1, 2, 3)
.parallel() // this causes .combiner() to be called
.collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
.isExactlyInstanceOf(IllegalStateException.class)
.hasCauseExactlyInstanceOf(IllegalStateException.class)
.hasStackTraceContaining("Duplicate key");
你如何使用它?好吧,就像测试显示的那样,只需使用它而不是 toMap()
。这使得调用代码看起来尽可能干净。
编辑: 下面实现了Holger的想法,添加了一个测试方法
【讨论】:
组合器不检查重复键。如果你想避免检查每个键,你可以使用(map1, map2) -> int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1;
@Holger 是的,没错。特别是因为accumulator()
实际上确实检查了这一点。也许我应该做一些并行流一次:)【参考方案5】:
public static <T, K, V> Collector<T, HashMap<K, V>, HashMap<K, V>> toHashMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper
)
return Collector.of(
HashMap::new,
(map, t) -> map.put(keyMapper.apply(t), valueMapper.apply(t)),
(map1, map2) ->
map1.putAll(map2);
return map1;
);
public static <T, K> Collector<T, HashMap<K, T>, HashMap<K, T>> toHashMap(
Function<? super T, ? extends K> keyMapper
)
return toHashMap(keyMapper, Function.identity());
【讨论】:
赞成,因为它可以编译。接受的答案无法编译,因为 Map::putAll 没有返回值。【参考方案6】:通过小调整保留所有问题 ID
Map<Integer, Boolean> answerMap =
answerList.stream()
.collect(Collectors.toMap(Answer::getId, a ->
Boolean.TRUE.equals(a.getAnswer())));
【讨论】:
我认为这是最好的答案 - 这是最简洁的答案,它解决了 NPE 问题。【参考方案7】:很抱歉重新提出一个老问题,但由于最近编辑说“问题”仍然存在于 Java 11 中,我觉得我想指出这一点:
answerList
.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
为您提供空指针异常,因为映射不允许 null 作为值。
这是有道理的,因为如果您在映射中查找键 k
并且它不存在,那么返回的值已经是 null
(参见 javadoc)。因此,如果您能够输入 k
值 null
,则地图看起来会表现得很奇怪。
正如 cmets 中有人所说,使用过滤很容易解决这个问题:
answerList
.stream()
.filter(a -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
这样,地图中不会插入null
值,并且当您在地图中查找没有答案的ID 时,您仍然会得到null
作为“值”。
我希望这对每个人都有意义。
【讨论】:
如果地图不允许空值是有意义的,但确实如此。你可以做answerMap.put(4, null);
没有任何问题。您是对的,使用您提出的解决方案,如果 anserMap.get() 不存在,您将获得相同的结果,就好像该值将作为 null 插入一样。但是,如果您遍历地图的所有条目,则显然存在差异。【参考方案8】:
如果值是一个字符串,那么这可能有效:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
【讨论】:
只有在您可以修改数据时才有效。下游方法可能需要空值而不是空字符串。【参考方案9】:Collectors
的静态方法是不可能的。 toMap
的 javadoc 解释说 toMap
是基于 Map.merge
的:
@param mergeFunction 一个合并函数,用于解决与同一键关联的值之间的冲突,提供给
Map#merge(Object, Object, BiFunction)
Map.merge
的 javadoc 说:
@throws NullPointerException 如果指定键为 null 并且此映射 不支持空键或 值 或 remappingFunction is 空
您可以通过使用列表的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());
【讨论】:
merge的javadoc中指定,toMap的doc中没有说明 从来没有想过map中的null值会对标准API产生如此大的影响,我宁愿认为它是一个缺陷。 实际上 API 文档没有说明任何关于Map.merge
的使用。这个恕我直言是实施中的一个缺陷,它限制了一个完全可以接受的被忽略的用例。 toMap
的重载方法确实声明了 Map.merge
的使用,但不是 OP 正在使用的方法。
@Jasper 甚至还有错误报告bugs.openjdk.java.net/browse/JDK-8148463
我不在乎引擎盖下有什么。在阅读 Javadoc 时,我只关心合同。如果任何元素为空,Javadoc 应该说它 throws NullPointerException
!【参考方案10】:
这里的收集器比@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;
);
我们只需将null
替换为一些自定义对象none
并在finisher 中执行相反的操作。
【讨论】:
【参考方案11】:我写了一个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()
调用替换为对该函数的调用,它就会解决问题。
【讨论】:
但是允许null
值和使用putIfAbsent
不能很好地结合使用。当它们映射到null
时,它不会检测到重复键...【参考方案12】:
根据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,所以我不知道是否有更好的方法来修复它,但修复它有点困难。
你可以这样做:
使用过滤器过滤所有NULL值,在javascript代码中检查服务器是否没有为这个id发送任何应答意味着他没有回复它。
类似这样的:
Map<Integer, Boolean> answerMap =
answerList
.stream()
.filter((a) -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
或者使用 peek,它用于改变元素的流元素。使用 peek 您可以将答案更改为更适合 map 的内容,但这意味着稍微修改您的逻辑。
听起来如果你想保留当前的设计,你应该避免Collectors.toMap
【讨论】:
以上是关于具有空条目值的 Collectors.toMap 中的 NullPointerException的主要内容,如果未能解决你的问题,请参考以下文章
java8的stream中Collectors.toMap空指针问题
Collectors.toMap() keyMapper——更简洁的表达方式?
List 转 Map 之 Collectors.toMap()