Collectors.toMap中的Java 8 NullPointerException


331

如果其中一个值为“ null”,则Java 8 Collectors.toMap会抛出a NullPointerException。我不了解这种行为,地图可以包含空指针作为值,而没有任何问题。是否有充分的理由为什么值不能为null 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中仍然存在此问题。


5
null总是有点问题,例如在TreeMap中。也许是一个尝试的好时机Optional<Boolean>?否则,请拆分并使用过滤器。
Joop Eggen

5
@JoopEggen null可能是密钥的问题,但在这种情况下,这就是值。
贡塔德2014年

并非所有地图都存在问题nullHashMap例如可以具有一个null键和任意数量的null值,您可以尝试Collector使用创建自定义HashMap而不是使用默认自定义。
kajacx

2
@kajacx但是默认实现是HashMap-如stacktrace的第一行所示。问题不是a Map不能保存null值,而是Map#mergefunction 的第二个参数不能为null。
czerny

就个人而言,在给定的情况下,我将使用非流解决方案,或者如果输入是并行的,则使用forEach()。以下基于短流的不错的解决方案可能会有糟糕的性能。
Ondra参观Žižka

Answers:


301

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

教程对我的帮助最大。)


3
@Jagger是的,供应商的定义(第一个参数)是不传递任何参数并返回结果的函数,因此,针对您的案例的lambda将是() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)创建不区分大小写的Stringkeyed TreeMap
Brett Ryan

2
这是正确的答案,恕我直言,对于默认的非重载版本,JDK应该做什么。也许合并速度更快,但是我还没有测试。
Brett Ryan

1
我只好以编译,这样指定类型参数: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)
AnthonyO。16年

2
对于大输入,这可能会非常慢。您创建一个HashMap,然后putAll()为每个条目调用。就个人而言,在给定的情况下,我将采用非流解决方案,或者forEach()输入是并行的。
Ondra参观Žižka

3
请注意,此解决方案的行为与原始toMap实现不同。原始实现检测到重复的密钥并抛出IllegalStatException,但是此解决方案无提示地接受最新的密钥。Emmanuel Touzery的解决方案(stackoverflow.com/a/32648397/471214)更加接近原始行为。
mmdemirbas

174

无法使用的静态方法Collectors。的javadoc toMap解释toMap基于Map.merge

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

和的javadoc Map.merge说:

@throws NullPointerException如果指定键为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
在那种情况下,我宁愿使用老式的for-each。我应该认为这是toMerge中的错误吗?因为使用此合并功能实际上是实现细节,还是不允许toMap处理空值的一个很好的理由?
Jasper 2014年

6
它是在merge的javadoc中指定的,但未在toMap的文档中声明
Jasper

119
从来没有想过map中的null值会对标准API产生如此大的影响,我宁愿将其视为缺陷。
Askar Kalykov 2015年

16
实际上,API文档并未说明有关的使用情况Map.merge。此恕我直言是实现中的一个缺陷,它限制了一个被完全忽略的完美用例。toMapdo 的重载方法说明Map.merge了OP正在使用的方法,但没有说明。
Brett Ryan

11
@Jasper甚至有错误报告bugs.openjdk.java.net/browse/JDK-8148463
像素

23

我编写了一个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()调用替换为对该函数的调用即可解决问题。


1
但是,允许null值和使用putIfAbsent不能一起很好地发挥作用。当它们映射到null…… 时,它不会检测到重复的密钥
Holger

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 (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
组合器不检查重复的密钥。如果您想避免检查每个键,则可以使用类似(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
Holger

@Holger Yep,是的。特别是因为accumulator()实际上确实要检查。也许我应该做一次并行流操作:)
sjngm

7

这是一个比@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然后在装订器中进行相反的操作。


5

如果该值为String,则可能会起作用: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))


4
仅当您可以修改数据时,该方法才有效。下游方法可能期望使用空值而不是空字符串。
Sam Buchmiller

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,所以我不知道是否有更好的方法来修复它,但是修复它有点困难。

您可以这样做:

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

像这样:

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

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

听起来如果您想保留当前设计,应该避免 Collectors.toMap


3

我对Emmanuel Touzery的实现进行了一些修改。

这个版本;

  • 允许空键
  • 允许空值
  • 与原始JDK实现一样,检测重复的键(即使它们为null),并抛出IllegalStateException。
  • 当键已经映射到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);
                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))
    );
}

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不会在地图中插入任何值,并且null在查找地图中没有答案的ID时,仍会得到“值”。

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


1
如果映射不允许空值,但允许空值将是有意义的。您可以做到answerMap.put(4, null);没有任何问题。正确的是,使用建议的解决方案,如果不存在anserMap.get(),则将获得与将值插入为null相同的结果。但是,如果您遍历地图的所有条目,则显然会有区别。
贾斯珀(Jasper)'18

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

1
赞成,因为这可以编译。接受的答案无法编译,因为Map :: putAll没有返回值。
Taugenichts

0

稍作调整即可保留所有问题ID

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

我认为这是最好的答案-这是最简洁的答案,并且可以解决NPE问题。
LConrad

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

1
无用的答案,为什么要摆脱null才能解决此问题?这是Collectors.toMap()不为空值的问题
Enerccio

@Enerccio冷静下来,哥们!依赖空值不是一个好习惯。如果您使用了Optional,那么您一开始就不会遇到NPE。阅读可选用法。
TriCore '18

1
那为什么呢?Null值很好,这是未记录的库。可选的很好,但不是到处都是。
Enerccio
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.