Collectors.toMap 中的 Java 8 NullPointerException

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24630963/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-14 13:34:01  来源:igfitidea点击:

Java 8 NullPointerException in Collectors.toMap

javanullpointerexceptionjava-8java-streamcollectors

提问by Jasper

The Java 8 Collectors.toMapthrows a NullPointerExceptionif one of the values is 'null'. I don't understand this behaviour, maps can contain null pointers as value without any problems. Is there a good reason why values cannot be null for Collectors.toMap?

如果其中一个值为“null”,则Java 8Collectors.toMap会抛出一个NullPointerException。我不明白这种行为,地图可以包含空指针作为值而没有任何问题。值不能为空有充分的理由Collectors.toMap吗?

Also, is there a nice Java 8 way of fixing this, or should I revert to plain old for loop?

另外,是否有一个很好的 Java 8 方法来解决这个问题,还是我应该恢复到普通的 for 循环?

An example of my problem:

我的问题的一个例子:

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

Stacktrace:

堆栈跟踪:

Exception in thread "main" java.lang.NullPointerException
    at java.util.HashMap.merge(HashMap.java:1216)
    at java.util.stream.Collectors.lambda$toMap8(Collectors.java:1320)
    at java.util.stream.Collectors$$Lambda/1528902577.accept(Unknown Source)
    at java.util.stream.ReduceOpsReducingSink.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)

This problem still exists in Java 11.

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

采纳答案by kajacx

You can work around this known bugin OpenJDK with this:

您可以使用以下方法解决OpenJDK 中的这个已知错误

Map<Integer, Boolean> collect = list.stream()
        .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);

It is not that much pretty, but it works. Result:

它不是那么漂亮,但它有效。结果:

1: true
2: true
3: null

(thistutorial helped me the most.)

教程对我帮助最大。)

回答by gontard

It is not possible with the static methods of Collectors. The javadoc of toMapexplains that toMapis based on Map.merge:

的静态方法是不可能的CollectorstoMap解释的 javadoctoMap基于Map.merge

@param mergeFunction a merge function, used to resolve collisions between values associated with the same key, as supplied to Map#merge(Object, Object, BiFunction)}

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

and the javadoc of Map.mergesays:

和 javadocMap.merge说:

@throws NullPointerException if the specified key is null and this map does not support null keys or the valueor remappingFunction isnull

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

You can avoid the for loop by using the forEachmethod of your list.

您可以使用forEach列表中的方法避免 for 循环。

Map<Integer,  Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));

but it is not really simple than the old way:

但这并不比旧方式简单:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}

回答by Marco Acierno

According to the Stacktrace

根据 Stacktrace

Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1216)
at java.util.stream.Collectors.lambda$toMap8(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda/391359742.accept(Unknown Source)
at java.util.stream.ReduceOpsReducingSink.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 is called the map.merge

什么时候被称为 map.merge

        BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);

It will do a nullcheck as first thing

它会做一个null检查作为第一件事

if (value == null)
    throw new NullPointerException();

I don't use Java 8 so often so i don't know if there are a better way to fix it, but fix it is a bit hard.

我不经常使用 Java 8,所以我不知道是否有更好的方法来修复它,但修复它有点困难。

You could do:

你可以这样做:

Use filter to filter all NULL values, and in the Javascript code check if the server didn't send any answer for this id means that he didn't reply to it.

使用 filter 过滤所有 NULL 值,并在 Javascript 代码中检查服务器是否没有为此 id 发送任何答案意味着他没有回复它。

Something like this:

像这样的东西:

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

Or use peek, which is used to alter the stream element for element. Using peek you could change the answer to something more acceptable for map but it means edit your logic a bit.

或者使用 peek,它用于更改元素的流元素。使用 peek 您可以将答案更改为地图更可接受的答案,但这意味着稍微编辑您的逻辑。

Sounds like if you want to keep the current design you should avoid Collectors.toMap

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

回答by Emmanuel Touzery

I wrote a Collectorwhich, unlike the default java one, does not crash when you have nullvalues:

我写了一个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;
            });
}

Just replace your Collectors.toMap()call to a call to this function and it'll fix the problem.

只需将您的Collectors.toMap()调用替换为对此函数的调用即可解决问题。

回答by Tagir Valeev

Here's somewhat simpler collector than proposed by @EmmanuelTouzery. Use it if you like:

这是比@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;
            });
}

We just replace nullwith some custom object noneand do the reverse operation in the finisher.

我们只是null用一些自定义对象替换none并在完成器中执行相反的操作。

回答by TriCore

NullPointerException is by far the most frequently encountered exception (at least in my case). To avoid this I go defensive and add bunch of null checks and I end up having bloated and ugly code. Java 8 introduces Optional to handle null references so you can define nullable and non-nullable values.

NullPointerException 是迄今为止最常遇到的异常(至少在我的情况下)。为了避免这种情况,我采取防御措施并添加一堆空检查,最终我得到了臃肿和丑陋的代码。Java 8 引入了 Optional 来处理空引用,因此您可以定义可为空和不可为空的值。

That said, I would wrap all the nullable references in Optional container. We should also not break backward compatibility as well. Here is the code.

也就是说,我会将所有可为空的引用包装在 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);
    }
}

回答by Gnana

If the value is a String, then this might work: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

如果该值是一个字符串,那么这可能有效: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

回答by sjngm

Yep, a late answer from me, but I think it may help to understand what's happening under the hood in case anyone wants to code some other Collector-logic.

是的,我的回答很晚,但我认为如果有人想编写其他Collector逻辑,了解幕后发生的事情可能会有所帮助。

I tried to solve the problem by coding a more native and straight forward approach. I think it's as direct as possible:

我试图通过编写一种更原生、更直接的方法来解决这个问题。我认为它尽可能直接:

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

    };
  }

}

And the tests using JUnit and assertj:

以及使用 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");
  }

And how do you use it? Well, just use it instead of toMap()like the tests show. This makes the calling code look as clean as possible.

你如何使用它?好吧,只是使用它而不是toMap()像测试显示的那样。这使调用代码看起来尽可能干净。

EDIT:
implemented Holger's idea below, added a test method

编辑:
在下面实现了 Holger 的想法,添加了一个测试方法

回答by sigirisetti

Retaining all questions ids with small tweak

通过小调整保留所有问题 ID

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

回答by Luca

Sorry to reopen an old question, but since it was edited recently saying that the "issue" still remains in Java 11, I felt like I wanted to point out this:

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

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

gives you the null pointer exception because the map does not allow null as a value. This makes sense because if you look in a map for the key kand it is not present, then the returned value is already null(see javadoc). So if you were able to put in kthe value null, the map would look like it's behaving oddly.

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

As someone said in the comments, it's pretty easy to solve this by using filtering:

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

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

in this way no nullvalues will be inserted in the map, and STILL you will get nullas the "value" when looking for an id that does not have an answer in the map.

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

I hope this makes sense to everyone.

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