在使用 Java 的新特性 Collectors.toMap() 將 List 轉換為 Map 時存在一些不容易發現的問題,這裡總結一下備查。
空指標風險
java.lang.NullPointerException
當 List 中有 null 值的時候,使用 Collectors.toMap() 轉為 Map 時,會報 java.lang.NullPointerException,如下:
List<SdsTest> sdsTests = new ArrayList<>(); SdsTest sds1 = new SdsTest("aaa","aaa"); SdsTest sds2 = new SdsTest("bbb",null); sdsTests.add(sds1); sdsTests.add(sds2); Map<String, String> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge)); System.out.println(map.toString()); --------- 執行錯誤: Exception in thread "main" java.lang.NullPointerException at java.util.HashMap.merge(HashMap.java:1216) at java.util.stream.Collectors.lambda$toMap$150(Collectors.java:1320) .....
原因是 toMap()
方法中使用 Map.merge()
方法合併時,merge 不允許 value 為 null 導致的,原始碼如下:
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { Objects.requireNonNull(remappingFunction); // 在這裡判斷了value不可為null Objects.requireNonNull(value); V oldValue = get(key); V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value); ...
解決方法
- 業務控制不要出現 Null 值【有 Null 的地方,可以賦值預設值】
- 在轉換時加判斷,如果為 null,則給一個預設值
Map<String, String> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, sdsTest -> sdsTest.getAge() == null ? "0" : sdsTest.getAge()));
- 使用 collect(..) 構建,允許空值
Map<String, String> nmap = sdsTests.stream().collect(HashMap::new,(k, v) -> k.put(v.getName(), v.getAge()), HashMap::putAll); // TODO 下游業務從Map取值要做NPE判斷
- 使用 Optional 對值進行包裝
Map<String, Optional<String>> opmap = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, sdsTest -> Optional.ofNullable(sdsTest.getAge()))); System.out.println("bbb.age=" + opmap.get("bbb").orElse("0")); ------------ 輸出: bbb.age=0
建議
- 優先業務控制,儘量避免 List 中存在 Null
- 其次推薦第 4 種方法【使用 Optional 對值進行包裝】,能很好的避免 NPE 問題
key重複風險
java.lang.IllegalStateException: Duplicate key xx
當 List 中有重複值的時候,使用 Collectors.toMap() 轉為 Map 時,會報:java.lang.IllegalStateException: Duplicate key xx,例如
List<SdsTest> sdsTests = new ArrayList<>(); SdsTest sds1 = new SdsTest("aaa","aaa"); SdsTest sds2 = new SdsTest("aaa","ccc"); sdsTests.add(sds1); sdsTests.add(sds2); Map<String, String> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge)); System.out.println(map.toString()); --------- 執行錯誤: Exception in thread "main" java.lang.IllegalStateException: Duplicate key aaa at java.util.stream.Collectors.lambda$throwingMerger$92(Collectors.java:133) at java.util.stream.Collectors$$Lambda$6/1177096266.apply(Unknown Source) at java.util.HashMap.merge(HashMap.java:1245) .....
原因是兩個引數的toMap(xx, xx)方法, 當出現重複key觸發merge時,直接丟擲異常。原始碼如下:
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { // 注意這裡的throwingMerger() return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new); }
接下來我們看throwingMerger() 方法:【注意方法註釋】
/** * Returns a merge function, suitable for use in * {@link Map#merge(Object, Object, BiFunction) Map.merge()} or * {@link #toMap(Function, Function, BinaryOperator) toMap()}, which always * throws {@code IllegalStateException}. This can be used to enforce the * assumption that the elements being collected are distinct. * * @param <T> the type of input arguments to the merge function * @return a merge function which always throw {@code IllegalStateException} */ private static <T> BinaryOperator<T> throwingMerger() { return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }; } ...
解決方法
- 業務控制儘量不要出現重複值
- 出現重複 key 時,使用後面的 value 覆蓋前面的 value
SdsTest sds1 = new SdsTest("aaa","aaa"); SdsTest sds2 = new SdsTest("bbb","bbb"); SdsTest sds3 = new SdsTest("aaa","ccc"); sdsTests.add(sds1); sdsTests.add(sds2); sdsTests.add(sds3); // 寫法一 Map<String, String> nmap = sdsTests.stream().collect(HashMap::new,(k, v) -> k.put(v.getName(), v.getAge()), HashMap::putAll); System.out.println("nmap->:" + nmap.toString()); // 寫法二 Map<String, String> nmap1 = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge, (k1, k2) -> k2)); System.out.println("nmap1->:" + nmap1.toString()); ... ---------------------- 輸出: nmap->:{aaa=ccc, bbb=bbb} nmap1->:{aaa=ccc, bbb=bbb}
- 出現重複 key 時,把對應的 value 拼接起來
... Map<String, String> nmap1 = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge, (k1, k2) -> k1 + "," + k2)); System.out.println("nmap1->:" + nmap1.toString()); ... ---------------- 輸出: nmap1->:{aaa=aaa,ccc, bbb=bbb}
- 把重複 key 的值拼成一個集合
...... Map<String, List<String>> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, s -> { List<String> ages = new ArrayList<>(); ages.add(s.getAge()); return ages; }, (List<String> v1, List<String> v2) -> { v1.addAll(v2); return v1; })); System.out.println("map->"+map.toString()); ------------ 輸出: map->{aaa=\[aaa, ccc\], bbb=\[bbb\]}
建議:
- 優先業務控制,儘量避免 List 中出現重複
- 若存在重複場景,則根據實際業務場景選擇具體方法【覆蓋、拼接、搞成集合】