常规 list 转 map 的方法:
Map<String, String> map = new HashMap<>();
for (User user : list) {
map.put(user.getName(), user.getAddress());
}
这种方式没什么问题,就是代码不够简洁美观,而且逼格不够高。可以通过 Java8 中的 Stream 流来轻松实现这个功能。
Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName,User::getAddress));
只需一行代码即可搞定,但是这种写法会出现两个问题:
IllegalStateException
异常,而常规的 Map 会用新的值覆盖旧的值;通过常规方法转 map 的时候并不会出现这两个问题,这样在不知情的情况下就会默认 stream 转 map 不会出现这种问题,从而碰到这个坑,看来减少代码行数是需要付出一定的代价的。
在解释为什么会出现上面的两个问题的原因之前,需要先理解 stream 流中 collect 的工作原理。collect 的工作原理简而言之就是分而治之。
就是把要参与计算的 N 个元素,放到 M 个容器中分别进行计算,将 M 个容器的计算结果再进行汇总。以将N个元素转换为 list 为例(对应于 toList()
方法),主要过程分为如下几步:
通过 java.util.stream.Collector
接口,我们可以为这样的一个计算过程指定容器的类型,每个容器内部的计算的方式,把容器计算结果汇总的方式等。下面代码展示该接口比较常用的几个函数。
public interface Collector<T, A, R> {
/**
* 这个函数用来创建容器并返回创建的容器
*/
Supplier<A> supplier();
/**
* 把容器内的元素进行指定的运算,并将结果放入该容器
*/
BiConsumer<A, T> accumulator();
/**
* 将任意两个容器的计算结果按指定的方式进行汇总,并将计算结果放入容器。
*/
BinaryOperator<A> combiner();
}
比较典型的 java.util.stream.Collectors
类中的许多方法都是用以上三个函数的组合来实现的。
通过阅读 toMap()
方法的源码,可以看到它的 combiner 中发现相同的 key 就会抛出 IllegalStateException
异常。
private static <T> BinaryOperator<T> throwingMerger() {
return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}
可以通过使用另一个重载方法,指定传入 mergeFunction 来解决,这个函数的作用是对于相同的 key,应该如何取舍,常用的就是传 (v1, v2) -> v1
,表示如果出现重复的 key,就使用最先出现的键值对,而放弃后来的键值对,即最开始的键值对不会被覆盖。
Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName,
User::getAddress, (v1, v2) -> v1));
现在我们解决了抛出 IllegalStateException
异常的问题,接下来看空指针是从哪抛出来的。
在 accumulator 中可以发现,toMap()
方法是将容器内的元素通过调用 map 接口中默认的 merge()
方法来实现的,其中对 map 的 value 做了非空校验。
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
// 如果 value 为空则抛出空指针异常
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value); // 如果建存在重复,则通过指定的规则决定用什么value
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
这里提供一种比较简单的解决办法,就是在调用 toMap()
方法时,预先处理空值。例如,
Map<String, String> map = list.stream().collect(Collectors.toMap(User::getName,
user -> user.getAddress() == null ? "" : user.getAddress(),
(v1, v2) -> v1));
这里确定使用的是字符串,就可以用空串替换 null 值。但是这种写法就使调用方法变得复杂了,如果仍然想像之前调用的方法一样,但是又不想抛出这两种异常,java.util.stream.Collector
接口为我们提供了一种解决方法,就是通过其中的 of()
方法或者实现 Collector
接口来自定义收集器。
自定义的核心是替换掉 accumulator 中的 merge()
方法,整体代码如下:
public class MyCollectors {
/**
* 大多数使用场景是不需要处理重复 key 的情况的,此时把 mergeFunction 作为默认参数,比较合适。
* 同时另一个重载方法可以定制化 mergeFunction
*/
public static <T, K, U>
Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, (v1, v2) -> v1, HashMap::new);
}
public static <T, K, U>
Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator = (map, element) -> mapMerge(keyMapper.apply(element),
valueMapper.apply(element), map, mergeFunction);
return Collector.of(mapSupplier, accumulator, mapMerger(mergeFunction), Collector.Characteristics.IDENTITY_FINISH);
}
/**
* 核心方法
*/
private static <K, V, M extends Map<K, V>> void mapMerge(K key, V value, M map,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
// 去掉对 value 的非空校验
V oldValue = map.get(key);
V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
// 这就和使用循环保存键值对的方法一样了
map.put(key, newValue);
}
private static <K, V, M extends Map<K, V>>
BinaryOperator<M> mapMerger(BinaryOperator<V> mergeFunction) {
return (m1, m2) -> {
for (Map.Entry<K, V> e : m2.entrySet())
mapMerge(e.getKey(), e.getValue(), m1, mergeFunction);
return m1;
};
}
}
使用示例:
Map<String, String> map = list.stream().collect(MyCollectors.toMap(User::getName, User::getAddress));
使得 toMap() 方法回归到最纯粹的样子。