Collectors.toMap中的Java 8 NullPointerException

Jas*_*per 292 java nullpointerexception java-8 java-stream collectors

如果其中一个值为'null' ,Java 8 Collectors.toMap将抛出一个NullPointerException.我不明白这种行为,map可以包含null指针作为值而没有任何问题.有没有一个很好的理由为什么值不能为空Collectors.toMap

另外,是否有一个很好的Java 8方法来修复它,或者我应该恢复到普通的旧循环?

我的问题的一个例子:

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

堆栈跟踪:

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)
Run Code Online (Sandbox Code Playgroud)

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

kaj*_*acx 252

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

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

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

1: true
2: true
3: null
Run Code Online (Sandbox Code Playgroud)

(教程对我帮助最大.)

  • @Jagger是的,供应商的定义(第一个参数)是一个不传递参数并返回结果的函数,因此你的case的lambda将是`() - > new TreeMap <>(String.CASE_INSENSITIVE_ORDER)`创建一个不区分大小写的`String`键入`TreeMap`. (3认同)
  • 这是正确的答案,恕我直言,JDK应该为其默认的非重载版本做些什么.也许合并更快,但我还没有测试过. (2认同)
  • 对于大输入,这可能会很慢。您创建一个 `HashMap`,然后为每个条目调用 `putAll()`。就我个人而言,在给定的情况下,如果输入是并行的,我会选择非流解决方案,或者使用 `forEach()`。 (2认同)
  • 请注意,此解决方案的行为与原始toMap实现不同。原始实现检测到重复的密钥并引发IllegalStatException,但是此解决方案无提示地接受最新的密钥。Emmanuel Touzery的解决方案(/sf/answers/2285387821/)更加接近原始行为。 (2认同)

gon*_*ard 164

用静态方法是不可能的Collectors.toMap解释的javadoc toMap基于Map.merge:

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

和javadoc Map.merge说:

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

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

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

但它并不比旧方式简单:

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

  • 从来没有想过map中的null值会对标准API产生这样的影响,我宁愿将其视为一个缺陷. (115认同)
  • 实际上,API文档没有说明[`Map.merge`]的使用情况(http://docs.oracle.com/javase/8/docs/api/index.html?java/lang/Integer.html ).这个恕我直言是实施中的一个缺陷,它限制了一个被忽视的完全可接受的用例.`toMap`的重载方法确实使用`Map.merge`而不是OP正在使用的方法. (15认同)
  • @Jasper甚至有bug报告https://bugs.openjdk.java.net/browse/JDK-8148463 (11认同)
  • 它在merge的javadoc中指定,但在toMap的doc中没有说明 (6认同)
  • 我不在乎它在引擎盖下有什么。在阅读 Javadoc 时,我只关心合同。Javadoc 应该说,如果任何元素为 null,它就会“抛出 NullPointerException”! (6认同)
  • 在那种情况下,我宁愿使用老式的每个人.我应该认为这是toMerge中的错误吗?因为使用这个合并函数实际上是一个实现细节,或者是不允许toMap处理空值的一个很好的推理? (3认同)

Emm*_*ery 22

我写了一个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;
            });
}
Run Code Online (Sandbox Code Playgroud)

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

  • 但是允许“null”值和使用“putIfAbsent”并不能很好地结合在一起。当它们映射到“null”时,它不会检测到重复的键... (4认同)

sjn*_*ngm 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));
      }

    };
  }

}
Run Code Online (Sandbox Code Playgroud)

使用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");
  }
Run Code Online (Sandbox Code Playgroud)

你怎么用它?好吧,只需使用它而不是toMap()测试显示.这使得调用代码看起来尽可能干净.


mmd*_*bas 7

我稍微修改了Emmanuel Touzery 的 null-safe map Collectorimplementation

这个版本:

  • 允许空键
  • 允许空值
  • 检测重复键(即使它们为空)并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;
        }
    );
}
Run Code Online (Sandbox Code Playgroud)

单元测试:

@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))
    );
}
Run Code Online (Sandbox Code Playgroud)


Tag*_*eev 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;
            });
}
Run Code Online (Sandbox Code Playgroud)

我们只需更换null一些自定义对象none并在整理器中执行相反的操作.


Gna*_*ana 5

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

  • 只有当您可以修改数据时,这才有效。下游方法可能需要空值而不是空字符串。 (5认同)