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
}
Таким образом, требуется больше работы, но в лучшем случае будет работать (по крайней мере, по моим опытам).
- Используйте для просмотра.
Вы также можете использовать цикл 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);
}
}