[java] Collectors.toMap의 Java 8 NullPointerException

Java 8 은 값 중 하나가 ‘null’인 경우 Collectors.toMapa를 던집니다 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에 존재합니다.



답변

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

( 튜토리얼 가장 도움 되었습니다.)


답변

의 정적 메소드로는 불가능합니다 Collectors. 의 javadoc는 toMap다음을 toMap기반으로 설명합니다 Map.merge.

@param mergeFunction은 제공된 것과 동일한 키와 관련된 값 사이의 충돌을 해결하는 데 사용되는 병합 함수입니다. Map#merge(Object, Object, BiFunction)}

의 javadoc는 Map.merge말합니다 :

지정된 키가 널이고이 맵이 널 키를 지원하지 않거나 또는 remappingFunction
널인 경우 @throws NullPointerException

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


답변

내가 쓴 Collector하는, 당신이있을 때 기본 자바 하나는 달리, 충돌하지 않는 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()이 함수를 호출에 전화를하고이 문제를 해결할 수 있습니다.


답변

그렇다. 나의 대답은 늦었지만 누군가가 다른 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 아이디어를 구현하고 테스트 방법을 추가했습니다.


답변

@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마무리 장치에서 역 작업을 수행합니다.


답변

값이 문자열이면 다음과 같이 작동합니다.

map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))


답변

에 따르면 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