Java 8 NullPointerException в Collectors.toMap

331

Java 8 Collectors.toMapвыбрасывает, NullPointerExceptionесли одно из значений '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.

Джаспер
источник
5
nullвсегда было немного проблематично, как в TreeMap. Может быть, хороший момент, чтобы попробовать Optional<Boolean>? В противном случае разделите и используйте фильтр.
Joop Eggen
5
@JoopEggen nullможет быть проблемой для ключа, но в этом случае это значение.
Gontard
Не все карты имеют проблемы null, HashMapнапример, могут иметь один nullключ и любое количество nullзначений, вы можете попробовать создать пользовательский, Collectorиспользуя HashMapвместо того, чтобы использовать по умолчанию.
kajacx
2
@kajacx Но реализация по умолчанию HashMap- как показано в первой строке stacktrace. Проблема не в том, что значение Mapне может содержать null, а в том, что второй аргумент Map#mergeфункции не может быть нулевым.
Черни
Лично при данных обстоятельствах я бы выбрал не потоковое решение или forEach (), если ввод параллельный. Хорошие решения на основе коротких потоков ниже могут иметь ужасную производительность.
Ондра Жижка

Ответы:

302

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

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

kajacx
источник
3
@Jagger да, определение поставщика (первый аргумент) - это функция, которая не передает параметров и возвращает результат, поэтому лямбда для вашего случая будет заключаться () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)в создании Stringключа без учета регистра TreeMap.
Бретт Райан
2
Это правильный ответ, и IMHO, что JDK должен делать для своей не перегруженной версии по умолчанию. Может быть, слияние происходит быстрее, но я не проверял.
Бретт Райан
1
Я должен был указать параметры типа, чтобы составить, таким образом: Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. У меня было:incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map<java.lang.Integer,java.lang.Boolean>,java.util.Map<java.lang.Integer,java.lang.Boolean>) method java.util.Map.putAll(java.util.Map) is not applicable (actual and formal argument lists differ in length)
Энтони О.
2
Это может быть довольно медленно на большом входе. Вы создаете, HashMapа затем вызываете putAll()для каждой записи. Лично при определенных обстоятельствах я бы выбрал не потоковое решение, или forEach()если вход параллельный.
Ондра Жижка
3
Помните, что это решение ведет себя не так, как оригинальная реализация toMap. Исходная реализация обнаруживает дубликаты ключей и выдает исключение IllegalStatException, но это решение молча принимает последний ключ. Решение Эммануэля Тузери ( stackoverflow.com/a/32648397/471214 ) ближе к исходному поведению.
mmdemirbas
174

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

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

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

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

Вы можете избежать цикла 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());
}
Gontard
источник
3
В этом случае я предпочел бы использовать старомодный для каждого. Должен ли я считать это ошибкой в ​​toMerge? так как использование этой функции слияния действительно является деталью реализации, или это хорошая причина для того, чтобы не позволять toMap обрабатывать нулевые значения?
Джаспер
6
Это указано в javadoc слияния, но это не указано в документе toMap
Джаспер
119
Никогда не думал, что нулевые значения в карте окажут такое влияние на стандартный API, я бы скорее счел это недостатком.
Аскар Калыков
16
На самом деле в документации API ничего не говорится об использовании Map.merge. Это IMHO - недостаток в реализации, который ограничивает совершенно приемлемый вариант использования, который был упущен. Перегруженные методы toMapdo устанавливают использование, Map.mergeно не тот, который использует OP.
Бретт Райан
11
@Jasper есть даже отчет об
пиксель
23

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

Эммануэль Тузери
источник
1
Но разрешение nullценностей и их использование putIfAbsentплохо сочетаются друг с другом. Он не обнаруживает дубликаты ключей, когда они отображаются на null...
Хольгер
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));
      }

    };
  }

}

И тесты, использующие 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()как показывают тесты. Это делает вызывающий код максимально чистым.

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

sjngm
источник
1
Объединитель не проверяет дубликаты ключей. Если вы хотите избежать проверки каждого ключа, вы можете использовать что-то вроде(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
Holger
@ Хольгер Да, это правда. Тем более, что на accumulator()самом деле это проверяет. Может быть, я должен сделать несколько параллельных потоков один раз :)
sjngm
7

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

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

Гнана
источник
4
Это работает, только если вы в порядке с изменением данных. Нисходящие методы могут ожидать нулевые значения, а не пустые строки.
Сэм Бухмиллер
3

Согласно 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

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

Эта версия;

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

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

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

Лука
источник
1
Это имело бы смысл, если бы карта не допускала нулевые значения, но это так. Вы можете обойтись answerMap.put(4, null);без проблем. Вы правы в том, что с вашим предлагаемым решением вы получите тот же результат для anserMap.get (), если его нет, как если бы значение было вставлено как нулевое. Однако, если вы перебираете все записи на карте, очевидно, есть разница.
Джаспер
1
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());
}
Игорь Зубченок
источник
1
голосование, потому что это компилируется. Принятый ответ не компилируется, потому что Map :: putAll не имеет возвращаемого значения.
Таугенихтс
0

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

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
sigirisetti
источник
Я думаю, что это лучший ответ - это самый краткий ответ, и он решает проблему NPE.
LConrad
-3

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

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

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);
    }
}
TriCore
источник
1
бесполезный ответ, почему вы должны избавиться от нуля, чтобы это исправить? Это проблема Collectors.toMap()
ненулевых
@Enerccio успокойся, приятель !! Использование нулевых значений не является хорошей практикой. Если бы вы использовали Optional, вы бы не столкнулись с NPE. Читайте о необязательных использованиях.
TriCore
1
и почему так? Нулевое значение в порядке, проблема в недокументированной библиотеке. Необязательно это приятно, но не везде.
Enerccio