具有空条目值的 Collectors.toMap 中的 NullPointerException

Posted

技术标签:

【中文标题】具有空条目值的 Collectors.toMap 中的 NullPointerException【英文标题】:NullPointerException in Collectors.toMap with null entry values 【发布时间】:2014-08-29 03:18:39 【问题描述】:

如果其中一个值为nullCollectors.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&lt;Boolean&gt; 的好时机?否则拆分并使用过滤器。 @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 将是 () -&gt; new TreeMap&lt;&gt;(String.CASE_INSENSITIVE_ORDER) 以创建不区分大小写的String 键入TreeMap 这是正确的答案,恕我直言,JDK 应该为其默认的非重载版本做些什么。也许合并更快,但我没有测试过。 我必须指定类型参数才能编译,这样:Map&lt;Integer, Boolean&gt; collect = list.stream().collect(HashMap&lt;Integer, Boolean&gt;::new, (m,v)-&gt;m.put(v.getId(), v.getAnswer()), HashMap&lt;Integer, Boolean&gt;::putAll);。我有:incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map&lt;java.lang.Integer,java.lang.Boolean&gt;,java.util.Map&lt;java.lang.Integer,java.lang.Boolean&gt;) 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) -&gt; int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() &lt; 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)。因此,如果您能够输入 knull,则地图看起来会表现得很奇怪。

正如 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()

Collectors.toMap使用详解

Java中Collectors.toMap()的基本使用方法和使用场景

Java Collectors