it-swarm.com.ru

Java 8 NullPointerException в Collectors.toMap

Java 8 Collectors.toMap выбрасывает NullPointerException, если одно из значений 'null'. Я не понимаю этого поведения, карты могут содержать нулевые указатели в качестве значения без каких-либо проблем. Есть ли веская причина, почему значения не могут быть нулевыми для Collectors.toMap?

Кроме того, есть ли способ исправления этого в Nice 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.

225
Jasper

Это невозможно при использовании статических методов Collectors. Javadoc для toMap объясняет, что toMap основан на Map.merge :

@param mergeFunction - функция слияния, используемая для разрешения коллизий между значениями, связанными с одним и тем же ключом, как указано в Map#merge(Object, Object, BiFunction)}

и Javadoc Map.merge говорит: 

@throws NullPointerException, если указанный ключ является нулевым, и эта карта не поддерживает нулевые ключи или значение или remappingFunction isноль

Вы можете избежать цикла for, используя forEach метод вашего списка.

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

Вы можете обойти эту известную ошибку в 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

( это учебник помог мне больше всего.)

177
kajacx

Я написал 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() на вызов этой функции, и это решит проблему.

16
Emmanuel Touzery

Да, поздний ответ от меня, но я думаю, что это может помочь понять, что происходит под капотом в случае, если кто-то захочет кодировать какую-то другую 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 (map1, map2) -> {
          map1.putAll(map2);
          return map1;
        };
      }

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

И как ты это используешь? Ну, просто используйте его вместо toMap(), как показывают тесты. Это делает вызывающий код максимально чистым.

7
sjngm

Вот несколько более простой сборщик, чем предложенный @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
Tagir Valeev

Если значение является строкой, это может работать: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

4
Gnana

Согласно 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 так часто, поэтому я не знаю, есть ли лучший способ исправить это, но исправить это немного сложно.

Вы могли бы сделать:

Используйте фильтр, чтобы отфильтровать все значения NULL, и в коде Javascript проверьте, не отправил ли сервер какой-либо ответ на этот идентификатор, что означает, что он не ответил на него.

Что-то вроде этого:

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

Или используйте peek, который используется для изменения элемента потока для элемента. Используя peek, вы можете изменить ответ на что-то более приемлемое для карты, но это означает, что нужно немного изменить свою логику.

Похоже, если вы хотите сохранить текущий дизайн, вам следует избегать Collectors.toMap

3
Marco Acierno
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());
}
0
Igor Zubchenok

Извините, что снова открыл старый вопрос, но так как он был недавно отредактирован и сказал, что «проблема» все еще остается в Java 11, я почувствовал, что хочу указать на это:

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

дает вам исключение нулевого указателя, потому что карта не допускает нулевое значение как значение . Это имеет смысл, потому что если вы ищите на карте ключ k, а его нет, возвращаемое значение уже null (см. javadoc ). Поэтому, если бы вы смогли ввести k значение null, карта выглядела бы странно.

Как кто-то сказал в комментариях, это довольно легко решить с помощью фильтрации:

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

таким образом никакие значения null не будут вставлены в карту, и ВСЕГДА вы получите null в качестве «значения» при поиске идентификатора, который не имеет ответа на карте.

Я надеюсь, что это имеет смысл для всех.

0
Luca

Сохранение всех идентификаторов вопросов с небольшим твиком

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