在收集器中的NullPointerException。使用空条目值的toMap

如果其中一个值是null,则Collectors.toMap抛出NullPointerException。我不理解这种行为,映射可以包含空指针作为值而没有任何问题。有一个很好的理由为什么Collectors.toMap的值不能为空?

此外,是否有一个很好的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)

这个问题在Java 11中仍然存在。

158567 次浏览

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

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

Map.merge的javadoc说:

@抛出NullPointerException,如果指定的键为空,并且此映射为空 不支持空键或的值或remappingFunction 空< / >强

你可以使用列表的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());
}

根据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

你可以在OpenJDK中使用以下方法来解决这个已知的错误:

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

(教程对我帮助最大。)

编辑:

Collectors.toMap不同的是,如果你多次使用相同的键,它将无声地替换值,正如@mmdemirbas在评论中指出的那样。如果你不想要这个,看看评论中的链接。

我写了一个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()调用替换为对这个函数的调用,它就会解决问题。

这里有一个比@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,并在结束器中执行相反的操作。

如果值是一个字符串,那么这可能是有效的: .stream map.entrySet () () .collect(收藏者。toMap (e→e. getkey (), e ->Optional.ofNullable (e.getValue ()) .orElse (" "))) < /代码> < / p >

是的,这是我迟来的回答,但我认为这可能有助于理解在外壳下发生的事情,以防有人想要编码一些其他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()。这使得调用代码看起来尽可能干净。

< p >编辑:< br > 下面实现Holger的想法,增加了一个测试方法

保留所有问题id与小调整

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

很抱歉重新打开一个老问题,但由于最近编辑说“问题”仍然存在于Java 11中,我觉得我想指出这一点:

answerList
.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

为您提供空指针异常,因为映射不允许null作为值。 这是有意义的,因为如果你在映射中寻找键k而它不存在,那么返回值已经是null(参见javadoc)。因此,如果你能够在k中放入值null,映射看起来会表现得很奇怪

就像有人在评论中说的,使用过滤很容易解决这个问题:

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

这样就不会在映射中插入null值,并且当你在映射中寻找一个没有答案的id时,仍然会得到null作为“值”。

我希望这对每个人都有意义。

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());
}

我稍微修改了Emmanuel Touzery的空安全映射Collector实现

这个版本:

  • 允许空键
  • 允许空值
  • 检测重复的键(即使它们为空)并像原始JDK实现一样抛出IllegalStateException
  • 当键已经映射到空值时,也会检测重复的键。换句话说,将具有空值的映射与无映射分开
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))
);
}

为了完整起见,我发布了一个带有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;
});
}