Stream将List转成Map的坑

1. 背景

常规 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));

只需一行代码即可搞定,但是这种写法会出现两个问题:

  1. 如果列表中的 key 不唯一的时候,会抛出 IllegalStateException 异常,而常规的 Map 会用新的值覆盖旧的值;
  2. 如果列表中的 value 为 null 时,会抛出空指针异常,而常规的 Map 的键和值都是支持 null 的。

通过常规方法转 map 的时候并不会出现这两个问题,这样在不知情的情况下就会默认 stream 转 map 不会出现这种问题,从而碰到这个坑,看来减少代码行数是需要付出一定的代价的。

2. collect 的原理分析

在解释为什么会出现上面的两个问题的原因之前,需要先理解 stream 流中 collect 的工作原理。collect 的工作原理简而言之就是分而治之
在这里插入图片描述
就是把要参与计算的 N 个元素,放到 M 个容器中分别进行计算,将 M 个容器的计算结果再进行汇总。以将N个元素转换为 list 为例(对应于 toList() 方法),主要过程分为如下几步:

  1. 程序会把 N 个元素分成 M 份,分别放进对应的容器中;
  2. 每个容易都将自己所拥有的元素转成 list,此时就会存在 M 个 list,总的元素个数是 N 个;
  3. 把 M 个容器中的 list 合并成一个 list。

通过 java.util.stream.Collector 接口,我们可以为这样的一个计算过程指定容器的类型,每个容器内部的计算的方式,把容器计算结果汇总的方式等。下面代码展示该接口比较常用的几个函数。

public interface Collector<T, A, R> {
    /**
     * 这个函数用来创建容器并返回创建的容器
     */
    Supplier<A> supplier();

    /**
     * 把容器内的元素进行指定的运算,并将结果放入该容器
     */
    BiConsumer<A, T> accumulator();

    /**
     * 将任意两个容器的计算结果按指定的方式进行汇总,并将计算结果放入容器。
     */
    BinaryOperator<A> combiner();
}

比较典型的 java.util.stream.Collectors 类中的许多方法都是用以上三个函数的组合来实现的。

3. 问题的分析及解决办法

通过阅读 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 接口来自定义收集器。

4. 自定义 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() 方法回归到最纯粹的样子。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值