Java 8 NullPointerException в Collectors.toMap

Java 8 Collectors.toMap выдает NullPointerException если одно из значений - "null". Я не понимаю этого поведения, карты могут содержать нулевые указатели как ценность без каких-либо проблем. Есть ли веская причина, почему значения не могут быть 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));
    }
}

Трассировки стека:

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.

222
08 июля '14 в 14:44
источник поделиться
12 ответов

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

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

а javadoc Map.merge говорит:

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

Вы можете избежать цикла 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());
}
145
08 июля '14 в 15:37
источник

Связанные вопросы


Похожие вопросы

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

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

171
08 июля '14 в 17:09
источник

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

15
18 сент. '15 в 12:21
источник

Да, последний ответ от меня, но я думаю, что это может помочь понять, что происходит под капотом, если кто-то хочет закодировать какой-то другой Collector -logic.

Я попытался решить проблему, кодируя более естественный и прямой подход. Я думаю, что он как можно более прямой:

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
08 апр. '17 в 22:27
источник

Вот несколько более простой коллекционер, чем предложил @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
18 сент. '15 в 12:52
источник

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

4
27 апр. '16 в 23:36
источник

В соответствии с 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
08 июля '14 в 15:38
источник
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
24 янв. '19 в 22:27
источник

Извините за возобновление старого вопроса, но, поскольку он недавно был отредактирован, что "проблема" по-прежнему остается в Java 11, мне показалось, что я хотел бы отметить это:

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

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

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

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

таким образом, никакие null значения не будут вставлены в карту, а STILL вы получите null как "значение" при поиске идентификатора, который не имеет ответа на карте.

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

0
07 нояб. '18 в 10:50
источник

Сохранение всех вопросов ids с небольшой настройкой

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
0
23 июня '17 в 11:06
источник

Значение bool никогда не должно быть null, поскольку boolean должен иметь 2 значения, а не 3. Возможно, вам стоит рассмотреть возможность использования другого подхода. В вашем случае, я думаю, вы не должны использовать Boolean-класс, вместо этого используйте примитивный тип boolean. (Узнайте больше о различиях).
Поскольку boolean дешевле, ваша программа может быть даже немного быстрее (особенно при работе со списками). Компиляция также предупредит вас, если вы попытаетесь установить ответ на null (<null> cannot be converted to boolean). Если вам нужны методы boolean, вы должны либо скрывать простой тип для класса в setAnswer (this.answer = new Boolean(answer), ваш бросок IllegalArgumentException, если boolean равно null). Если вам действительно нужно использовать трехзначную логику, вы можете либо спросить у Google, как реализовать его лучше, либо просто добавить третий метод (например, hasAnswer). Если вы предоставляете больше информации о том, что должен делать ответ, а что означает setAnswer(null), я мог бы предоставить вам лучший способ помочь вам.

Как отметил @Jasper, answer может быть оставлен без ответа, и если это так, то будет 0. Поскольку ответ будет преобразован в ответ JSON, он может быть прочитан другими языками, а некоторым есть проблемы, чтобы различать false и null (особенно потому, что вы часто используете такой код: if not boolValue then do... вместо if boolValue == false). Теперь у вас есть 2 подхода, чтобы исправить вашу проблему:

1:
Добавьте третий метод isAnswered, который возвращает true, если на вопрос был дан ответ или false, если это не так.

2:
Не делайте answer bool, вместо этого сделайте это String. Я думаю, что это лучший подход, потому что строки часто могут быть null (также не забывайте об этом, чтобы обернуть строку в Необязательном!), И я ожидаю, что ответ будет строкой, содержащей его ответ.

class Answer {
    private int id;
    private Optional<String> answer;

    Answer(int id, String answer) {
        setId(id);
        setAnswer(answer);
    }
    // setter/getter for id

    public Optional<String> getAnswer() {
        return answer;
    }

    public void setAnswer(String answer) {
        setAnswer(Optional.ofNullable(answer));
    }

    public void setAnswer(Optional<String> answer) {
        this.answer = answer;
    }
}

Убедитесь, что ваш JSON API корректно ведет себя с классом Java Option.

3:
Если ответ JSON должен знать только если он был прав, недействителен или не указан, используйте перечисление:

enum AnswerType {
    CORRECT, INCORRECT, NOT_GIVEN
}

Таким образом, требуется больше работы, но в лучшем случае будет работать (по крайней мере, по моим опытам).

  1. Используйте для просмотра.

Вы также можете использовать цикл for и создать карту вручную. Java8 List также предоставляет метод forEach, который действительно потрясающий (я видел его на многих других языках, и они действительно опрятные).

-

Вы также можете сделать шаг 2 с boolean, завернутым в Optional, но это часто завлекало меня в неприятные ошибки. Особенно, когда ответ JSON читается другими динамическими языками. Другой разработчик иногда просто заглядывает в документы и не замечает трехзначной логики и просто проверяет, истинно ли логическое (или даже хуже, если оно ложно). Это часто создает неприятные ошибки. Если значение представляет собой перечисление (или хотя бы строку с тремя заданными константами), разработчик должен покрыть все 3 случая в правильном переключателе/​​случае, потому что он будет более внимательно смотреть на результат констант.

-2
08 июля '14 в 15:08
источник

NullPointerException на сегодняшний день является наиболее часто встречающимся исключением (по крайней мере, в моем случае). Чтобы этого избежать, я иду защищаться и добавляю кучу нулевых проверок, и в итоге у меня раздутый и уродливый код. Java 8 вводит опцию для обработки нулевых ссылок, поэтому вы можете определить значения с нулевым и непустым значением.

Тем не менее, я бы обернул все нулевые ссылки в дополнительном контейнере. Мы также не должны нарушать обратную совместимость. Вот код.

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);
    }
}
-3
01 янв. '16 в 1:37
источник

Посмотрите другие вопросы по меткам или Задайте вопрос