Java 8 NullPointerException в коллекторах.toMap
Java 8 Collectors.toMap бросает a NullPointerException если одно из значений 'null'. Я не понимаю этого поведения, карты могут содержать нулевые указатели в качестве значения без каких-либо проблем. Есть ли веская причина, почему значения не могут быть 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));
}
}
Stacktrace:
Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1216)
at java.util.stream.Collectors.lambda$toMap8(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda/1528902577.accept(Unknown Source)
at java.util.stream.ReduceOpsReducingSink.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)
9 ответов:
это невозможно со статическими методами
Collectors. Документация javadoc в размереtoMapпоясняет, чтоtoMapна основеMap.merge:@param mergeFunction функция слияния, используемая для разрешения конфликтов между значениями, связанными с тем же ключом, что и
Map#merge(Object, Object, BiFunction)}и javadoc из
Map.mergeговорит:@бросает NullPointerException, если указанный ключ нуль и эта карта не поддерживает нулевые ключи или значение или remappingFunction и 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()); }
Вы можете сделать это:
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(этой учебник помог мне больше всего.)
Я написал
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()вызов этой функции и он будет исправить проблему.
вот несколько более простой коллектор, чем предложенный @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и выполните обратную операцию в финишере.
да, поздний ответ от меня, но я думаю, что это может помочь понять, что происходит под капотом, если кто-то хочет закодировать какой-то другой
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()как показывают тесты. Этот делает вызывающий код максимально чистым.
Если значение является строкой, то это может сработать:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
по словам
StacktraceException in thread "main" java.lang.NullPointerException at java.util.HashMap.merge(HashMap.java:1216) at java.util.stream.Collectors.lambda$toMap8(Collectors.java:1320) at java.util.stream.Collectors$$Lambda/391359742.accept(Unknown Source) at java.util.stream.ReduceOpsReducingSink.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.mergeBiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction);это
nullпроверьте как первоеif (value == null) throw new NullPointerException();Я не использую Java 8 так часто, поэтому я не знаю, есть ли лучший способ исправить это, но исправить это немного сложно.
вы могли бы сделать:
используйте фильтр для фильтрации всех нулевых значений, и в коде Javascript проверьте, не отправил ли сервер какой-либо ответ для этого идентификатора означает, что он не ответил оно.
что-то вроде этого:
Map<Integer, Boolean> answerMap = answerList .stream() .filter((a) -> a.getAnswer() != null) .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));или используйте peek,который используется для изменения элемента потока для элемента. Используя peek, вы можете изменить ответ на что-то более приемлемое для карты, но это означает, что вы немного отредактируете свою логику.
похоже, если вы хотите сохранить текущий дизайн, вы должны избегать
Collectors.toMap
сохранение всех идентификаторов вопросов с небольшой настройкой
Map<Integer, Boolean> answerMap = answerList.stream().collect(Collectors.toMap(Answer::getId, a -> Boolean.TRUE.equals(a.getAnswer())));
NullPointerException на сегодняшний день является наиболее часто встречающимся исключением (по крайней мере, в моем случае). Чтобы избежать этого, я защищаюсь и добавляю кучу нулевых проверок, и в итоге у меня раздутый и уродливый код. Java 8 вводит необязательный для обработки нулевых ссылок, так что вы можете определить nullable и non-nullable значения.
тем не менее, я бы обернул все nullable ссылки в необязательный контейнер. Мы также не должны нарушать обратную совместимость. Вот этот код.
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); } }
Comments