如果其中一个值为' null',则Java 8 Collectors.toMap
会抛出NullPointerException
。我不理解这种行为,映射可以包含空指针作为值而没有任何问题。是否有充分的理由说明Collectors.toMap
的值不能为空?
此外,是否有一个很好的Java 8方法来修复它,或者我应该恢复到普通的for 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中仍存在此问题。
答案 0 :(得分:196)
您可以使用以下方法解决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教程对我帮助最大。)
答案 1 :(得分:157)
使用Collectors
的静态方法是不可能的。 toMap
的javadoc解释toMap
基于Map.merge
:
@param mergeFunction一个合并函数,用于解决与提供给
的相同密钥关联的值之间的冲突Map#merge(Object, Object, BiFunction)}
和Map.merge
的javadoc说:
@throws NullPointerException如果指定的键为null且为此映射 不支持null键或值或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());
}
答案 2 :(得分:10)
是的,这是我的一个迟到的答案,但我认为,如果有人想要编写其他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的测试:
@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);
}
你怎么用它?好吧,只需使用它代替toMap()
,就像测试所示。这使得调用代码看起来尽可能干净。
答案 3 :(得分:5)
这里的收藏家比@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
,然后在整理器中执行相反的操作。
答案 4 :(得分:4)
如果值是String,那么这可能有效:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
答案 5 :(得分:3)
根据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,它用于改变元素的stream元素。使用peek,您可以将答案更改为地图更可接受的内容,但这意味着稍微编辑您的逻辑。
如果你想保留当前的设计,你应该避免使用Collectors.toMap
答案 6 :(得分:1)
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());
}
答案 7 :(得分:1)
我对Emmanuel Touzery's implementation做了一些修改。
此版本;
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);
if (map.containsKey(key)) {
throw new IllegalStateException(String.format("Duplicate key %s", key));
}
map.put(key, valueMapper.apply(item));
});
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))
);
}
答案 8 :(得分:0)
通过小调整保留所有问题ID
Map<Integer, Boolean> answerMap =
answerList.stream()
.collect(Collectors.toMap(Answer::getId, a ->
Boolean.TRUE.equals(a.getAnswer())));
答案 9 :(得分:0)
为了完整起见,我发布了一个带有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;
});
}
答案 10 :(得分:-1)
很抱歉再次打开一个旧问题,但是由于它最近被编辑说“问题”仍保留在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
作为“值”。
我希望这对所有人都有意义。
答案 11 :(得分:-3)
NullPointerException是迄今为止最常遇到的异常(至少在我的情况下)。为了避免这种情况,我采取防御措施并添加一堆空检查,最终我得到了臃肿和丑陋的代码。 Java 8引入了Optional来处理空引用,因此您可以定义可空值和不可空值。
那就是说,我会在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 (i.e. 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);
}
}