Collectors.toMap 中的 NullPointerException,条目值为 null

NullPointerException in Collectors.toMap with null entry values

提问人:Jasper 提问时间:7/8/2014 最后编辑:M. JustinJasper 更新时间:5/18/2023 访问量:188161

问:

Collectors.toMap如果其中一个值为 ,则抛出 。我不明白这种行为,地图可以包含空指针作为值,没有任何问题。值不能为空有充分的理由吗?NullPointerExceptionnullCollectors.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 中仍然存在。

java nullpointerexception java-stream 收集器

评论

6赞 Joop Eggen 7/8/2014
null总是有点问题,就像在 TreeMap 中一样。也许是尝试的好时机?否则,拆分并使用过滤器。Optional<Boolean>
17赞 gontard 7/8/2014
@JoopEggen对于键来说可能是一个问题,但在这种情况下,它是值。null
3赞 kajacx 7/8/2014
并非所有地图都有问题,例如可以有一个键和任意数量的值,您可以尝试使用 a 而不是使用默认的创建自定义。nullHashMapnullnullCollectorHashMap
6赞 czerny 5/13/2015
@kajacx 但默认实现是 - 如堆栈跟踪的第一行所示。问题不在于 a 不能保存值,而在于函数的第二个参数不能为 null。HashMapMapnullMap#merge
1赞 Ondra Žižka 11/30/2018
就个人而言,在给定的情况下,我会选择非流解决方案,如果输入是并行的,我会选择forEach()。下面基于短流的解决方案可能会有糟糕的性能。

答:

209赞 gontard 7/8/2014 #1

使用 的静态方法是不可能的。toMap 的 javadoc 解释说它基于 Map.mergeCollectorstoMap

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

Map.merge 的 javadoc 说:

@throws NullPointerException,如果指定的键为 null,并且此映射 不支持 null 键,或者 value 或 remappingFunction null

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

评论

3赞 Jasper 7/8/2014
在这种情况下,我宁愿使用老式的 for-each。我应该认为这是 toMerge 中的错误吗?因为使用此合并函数确实是一个实现细节,或者是不允许 toMap 处理 null 值的一个很好的理由?
163赞 Askar Kalykov 4/15/2015
从没想过 map 中的 null 值会对标准 API 产生如此大的影响,我宁愿将其视为一个缺陷。
26赞 Brett Ryan 10/19/2015
实际上,API 文档并没有说明任何关于使用 Map.merge 的信息。恕我直言,这是实现中的一个缺陷,它限制了一个完全可以接受的用例,但被忽视了。do 的重载方法声明了 OP 正在使用的那个,但未声明。toMapMap.merge
14赞 pixel 9/23/2016
@Jasper甚至还有错误报告 bugs.openjdk.java.net/browse/JDK-8148463
16赞 ACV 5/7/2019
我不在乎它在引擎盖下有什么。在阅读 Javadoc 时,我只关心契约。Javadoc 应该说,如果任何元素为 null!throws NullPointerException
3赞 Marco Acierno 7/8/2014 #2

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

When 被称为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,它用于更改元素的流元素。使用 peek,您可以将答案更改为更适合 map 接受的内容,但这意味着稍微编辑一下您的逻辑。

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

517赞 kajacx 7/8/2014 #3

您可以使用以下方法解决 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

(教程对我帮助最大。

编辑:

与 不同,如果您多次使用相同的键,这将静默地替换值,正如@mmdemirbas在评论中指出的那样。如果您不想要这个,请查看评论中的链接。Collectors.toMap

评论

3赞 Brett Ryan 10/19/2015
@Jagger是的,供应商的定义(第一个参数)是一个不传递参数并返回结果的函数,因此您的情况的 lambda 将是创建一个不区分大小写的键控。() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)StringTreeMap
2赞 Brett Ryan 10/19/2015
这是正确的答案,恕我直言,JDK 应该为其默认的非重载版本做什么。也许合并更快,但我还没有测试过。
1赞 Anthony O. 2/18/2016
我必须指定类型参数才能进行编译,这样: .我有: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)
3赞 Ondra Žižka 11/30/2018
在大输入时,这可能很慢。您创建一个条目,然后为每个条目调用。就个人而言,在特定情况下,我会选择非流解决方案,或者如果输入是并行的。HashMapputAll()forEach()
10赞 mmdemirbas 8/8/2019
请注意,此解决方案的行为与原始 toMap 实现不同。原始实现检测重复键并引发 IllegalStatException,但此解决方案以静默方式接受最新的键。Emmanuel Touzery 的解决方案 (stackoverflow.com/a/32648397/471214) 更接近原始行为。
32赞 Emmanuel Touzery 9/18/2015 #4

我写了一个,与默认的 java 不同,当您有值时不会崩溃:Collectornull

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

评论

6赞 Holger 8/9/2019
但是,允许值和使用并不能很好地结合在一起。当重复键映射到...nullputIfAbsentnull
11赞 Tagir Valeev 9/18/2015 #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;
            });
}

我们只是用一些自定义对象替换,然后在完成器中执行相反的操作。nullnone

4赞 Gnana 4/28/2016 #6

如果值是 String,则这可能有效: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

评论

7赞 Sam Buchmiller 3/14/2019
仅当您可以修改数据时,这才有效。下游方法可能需要 null 值,而不是空字符串。
12赞 sjngm 4/9/2017 #7

是的,我的回答很晚,但我认为如果有人想编写其他 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 (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的想法,增加了一个测试方法

评论

1赞 Holger 7/22/2019
合路器不检查重复的键。如果你想避免检查每个键,你可以使用类似的东西(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
0赞 sjngm 7/23/2019
@Holger 是的,这是真的。特别是因为实际上确实检查了这一点。也许我应该做一些并行流:)accumulator()
2赞 sigirisetti 6/23/2017 #8

保留所有问题 ID,并稍作调整

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

评论

0赞 LConrad 2/15/2018
我认为这是最好的答案 - 这是最简洁的答案,它解决了 NPE 问题。
0赞 sigirisetti 1/7/2022
诀窍是等于()处理null,所以以上就足够了
4赞 Luca 11/7/2018 #9

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

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

给出 null 指针异常,因为映射不允许将 null 作为值。 这是有道理的,因为如果您在映射中查找键,并且它不存在,则返回的值已经存在(请参阅 javadoc)。因此,如果您能够输入值,地图看起来会很奇怪。knullknull

正如有人在评论中所说,使用过滤很容易解决这个问题:

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

这样,不会在地图中插入任何值,并且在查找地图中没有答案的 id 时,您仍然会得到“值”。nullnull

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

评论

8赞 Jasper 11/7/2018
如果映射不允许 null 值,那将是有道理的,但它确实允许。你可以毫无问题地做。你是对的,在你提出的解决方案中,如果anserMap.get()不存在,你会得到相同的结果,就好像该值将入为null一样。但是,如果遍历映射的所有条目,则明显存在差异。answerMap.put(4, null);
2赞 Igor Zubchenok 1/25/2019 #10
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());
}

评论

1赞 Taugenichts 4/25/2019
点赞,因为这是编译的。接受的答案不会编译,因为 Map::p utAll 没有返回值。
9赞 mmdemirbas 8/8/2019 #11

我稍微修改了 Emmanuel Touzery 的 null-safe map Collector 实现

此版本:

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

评论

0赞 M. Justin 3/19/2021
对重复消息进行了轻微改进,以镜像 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赞 Henrik Langli 7/2/2020 #12

为了完整起见,我发布了一个带有 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;
    });
}
1赞 Simon 2/8/2023 #13

只需将可为 null 的值包装为 Optional。相当优雅的解决方法。

    listOfValues.stream()
    .collect(Collectors.toMap(e -> e.getKey(), e -> 
    Optional.ofNullable(e.getValue())))

评论

2赞 dukethrash 9/14/2023
则该值应为 null 时为 Optional