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.

Ответ 1

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

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

Ответ 2

Это невозможно при использовании статических методов 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());
}

Ответ 3

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

Ответ 4

Да, поздний ответ от меня, но я думаю, что это может помочь понять, что происходит под капотом в случае, если кто-то захочет закодировать какой-нибудь другой 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 (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() как показывают тесты. Это делает вызывающий код максимально чистым.

РЕДАКТИРОВАТЬ:
ниже реализована идея Хольгера, добавлен метод тестирования

Ответ 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;
            });
}

Мы просто заменяем null на некоторый пользовательский объект none и выполняем обратную операцию в финишере.

Ответ 6

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

Ответ 7

В соответствии с 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

Ответ 8

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

Ответ 9

Я немного изменил реализацию Эммануэля Тузери.

Эта версия;

  • Разрешить нулевые ключи
  • Допускает нулевые значения
  • Обнаруживает дубликаты ключей (даже если они нулевые) и создает исключение 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);
                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))
    );
}

Ответ 10

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

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

Ответ 11

Извините за возобновление старого вопроса, но, поскольку он недавно был отредактирован, что "проблема" по-прежнему остается в 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 как "значение" при поиске идентификатора, который не имеет ответа на карте.

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

Ответ 12

Значение 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 случая в правильном переключателе/​​случае, потому что он будет более внимательно смотреть на результат констант.

Ответ 13

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