Java 8 Collectors.toMap
zgłasza, NullPointerException
jeśli jedna z wartości ma wartość „null”. Nie rozumiem tego zachowania, mapy mogą zawierać zerowe wskaźniki jako wartość bez żadnych problemów. Czy istnieje dobry powód, dla którego wartości nie mogą mieć wartości null Collectors.toMap
?
Czy jest też dobry sposób na naprawienie tego w Javie 8, czy powinienem powrócić do zwykłej starej pętli for?
Przykład mojego problemu:
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));
}
}
Ślad stosu:
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)
Ten problem nadal występuje w Javie 11.
null
zawsze było trochę problematyczne, jak w TreeMap. Może miły moment na wypróbowanieOptional<Boolean>
? W przeciwnym razie podziel i użyj filtru.null
może stanowić problem dla klucza, ale w tym przypadku jest to wartość.null
,HashMap
na przykład może mieć jedennull
klucz i dowolną liczbęnull
wartości, możesz spróbować utworzyć niestandardowyCollector
za pomocąHashMap
zamiast zamiast domyślnego.HashMap
- jak pokazano w pierwszym wierszu stacktrace. Problemem nie jest to, że nieMap
można przechowywaćnull
wartości, ale że drugi argumentMap#merge
funkcji nie może mieć wartości NULL.Odpowiedzi:
Możesz obejść ten znany błąd w OpenJDK za pomocą:
Nie jest aż tak ładnie, ale działa. Wynik:
( ten samouczek najbardziej mi pomógł).
źródło
() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
utworzenieString
klucza bez rozróżniania wielkości literTreeMap
.Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);
. Miałem: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)
HashMap
a następnie dzwoniszputAll()
do każdego wpisu. Osobiście w danych okolicznościach wybrałbym rozwiązanie inne niż strumieniowe lubforEach()
jeśli dane wejściowe są równoległe.Nie jest to możliwe przy użyciu metod statycznych
Collectors
. JavadoctoMap
wyjaśnia, żetoMap
opiera się naMap.merge
:a javadoc
Map.merge
mówi:Możesz uniknąć pętli for, używając
forEach
metody z listy.ale nie jest to tak naprawdę proste jak dawniej:
źródło
Map.merge
. Ten IMHO jest wadą w implementacji, która ogranicza całkowicie dopuszczalny przypadek użycia, który został pominięty. Przeciążone metodytoMap
określają użycie,Map.merge
ale nie tego, którego używa OP.Napisałem taki,
Collector
który, w przeciwieństwie do domyślnego java, nie ulega awarii, gdy masznull
wartości:Wystarczy zastąpić
Collectors.toMap()
połączenie do połączenia z tą funkcją, a to rozwiąże problem.źródło
null
wartości i używanieputIfAbsent
nie działa dobrze razem. Nie wykrywa duplikatów kluczy, gdy są mapowane nanull
…Tak, późna odpowiedź ode mnie, ale myślę, że może pomóc zrozumieć, co dzieje się pod maską, na wypadek, gdyby ktoś chciał
Collector
napisać inną logikę.Próbowałem rozwiązać problem, kodując bardziej natywne i proste podejście. Myślę, że jest to tak bezpośrednie, jak to możliwe:
A testy przy użyciu JUnit i assertj:
A jak tego używasz? Cóż, po prostu użyj go zamiast
toMap()
jak pokazują testy. Dzięki temu kod wywołujący wygląda tak czysto, jak to możliwe.EDYCJA:
zaimplementowano poniżej pomysł Holgera, dodano metodę testową
źródło
(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
accumulator()
faktycznie to sprawdza. Może powinienem kiedyś zrobić kilka równoległych strumieni :)Oto nieco prostszy kolektor niż zaproponowany przez @EmmanuelTouzery. Użyj go, jeśli chcesz:
Po prostu zamieniamy na
null
jakiś niestandardowy obiektnone
i wykonujemy operację odwrotną w finiszerze.źródło
Jeśli wartością jest String, może to działać:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
źródło
Według
Stacktrace
Kiedy nazywa się
map.merge
Będzie to zrobić
null
czek jako pierwszą rzecząNie używam Java 8 tak często, więc nie wiem, czy jest lepszy sposób, aby to naprawić, ale naprawa jest trochę trudna.
Mógłbyś:
Użyj filtru, aby odfiltrować wszystkie wartości NULL, a w kodzie JavaScript sprawdź, czy serwer nie wysłał żadnej odpowiedzi na ten identyfikator, oznacza, że nie odpowiedział na to.
Coś takiego:
Lub użyj peek, który służy do zmiany elementu strumienia dla elementu. Za pomocą peek możesz zmienić odpowiedź na coś bardziej akceptowalnego dla mapy, ale oznacza to nieco edycję logiki.
Wygląda na to, że jeśli chcesz zachować obecny projekt, którego powinieneś unikać
Collectors.toMap
źródło
Lekko zmodyfikowałem implementację Emmanuela Touzery'ego .
Ta wersja;
Testy jednostkowe:
źródło
Przepraszam, że ponownie otworzyłem stare pytanie, ale ponieważ ostatnio edytowałem, że „problem” nadal występuje w Javie 11, czułem, że chciałbym to podkreślić:
daje wyjątek wskaźnika zerowego, ponieważ mapa nie dopuszcza wartości zerowej jako wartości. Ma to sens, ponieważ jeśli spojrzysz na mapę w poszukiwaniu klucza
k
i nie ma go, to zwrócona wartość jest jużnull
(patrz javadoc). Gdybyś więc mógł wprowadzićk
tę wartośćnull
, mapa wyglądałaby tak, jakby zachowywała się dziwnie.Jak ktoś powiedział w komentarzach, dość łatwo to rozwiązać za pomocą filtrowania:
w ten sposób żadne
null
wartości nie zostaną wstawione do mapy, a JESZCZE otrzymasznull
jako „wartość”, gdy szukasz identyfikatora, który nie ma odpowiedzi na mapie.Mam nadzieję, że ma to sens dla wszystkich.
źródło
answerMap.put(4, null);
bez żadnych problemów. Masz rację, że dzięki proponowanemu rozwiązaniu uzyskasz ten sam wynik dla anserMap.get (), jeśli nie jest obecny, jakby wartość została wstawiona jako null. Jeśli jednak powtórzysz wszystkie wpisy na mapie, to oczywiście będzie różnica.źródło
Zachowując wszystkie pytania identyfikatory z drobnymi poprawkami
źródło
NullPointerException jest zdecydowanie najczęściej spotykanym wyjątkiem (przynajmniej w moim przypadku). Aby tego uniknąć, wybieram defensywę i dodam kilka zerowych czeków, a ostatecznie mam rozdęty i brzydki kod. Java 8 wprowadza Opcjonalne do obsługi odwołań zerowych, dzięki czemu można zdefiniować wartości zerowalne i zerowalne.
To powiedziawszy, zawinię wszystkie zerowalne odwołania w opcjonalny pojemnik. Nie powinniśmy również łamać kompatybilności wstecznej. Oto kod.
źródło
Collectors.toMap()
wartościami