14-简化工作:Guava 中 Lists、Maps 实际工作运用和源码(集合)

注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

在日常工作中,我们经常会使用一些第三方的 API 来简化我们的工作,Guava 就是其中的一种,Guava 是 Google 开源的技术框架,使用率高,社区活跃度也高。

源码地址:https://github.com/google/guava

1 运用工厂模式进行初始化

在集合类初始化方面,Guava 比 Java 原生的 API 更加好用,还发明了很多新的功能,比如说在 JDK 7 之前,我们新建集合类时,声明和初始化都必须写上泛型类型,像这样:List<泛型> list = new ArrayList<泛型>();,JDK 7 之后有所改变,我们只需要在声明处写上泛型说明:List<泛型> list = new Arraylist<>();

Guava 提供了更加方便的使用姿势,采用了工厂模式,把集合创建的逻辑交给了工厂,开发者无需关注工厂底层是如何实现的,只需要关心工厂能生产什么,于是代码变成了这样:List<泛型> list = Lists.newArrayList();,Lists 就是 Guava 提供出来的,方便操作 List 的工具类。

这种写法其实就是一种简单的工厂模式,只需要简单的定义好工厂的入场和出参,就能对外隐藏其内部的创建逻辑,提供了更加方便的使用体验。

当然除了 Lists,Guava 还提供了很多,如 Maps、Sets 等等。

2 Lists

2.1 初始化

Lists 最大的功能就是帮助我们进行 list 的初始化,比如上面说的 newArrayList:

// 底层帮助我们写好了泛型,E 代表泛型,表示当前返回的泛型类型和声明的一致即可,
// 在编译的时候,会把泛型 E 转化成我们声明的 String
public static <E> ArrayList<E> newArrayList() {
	return new ArrayList<>();
}

如果你清楚 List 的大小,也可以这样做:

// 可以预估 list 的大小为 20
Lists.newArrayListWithCapacity(20);
/ 不太肯定 list 大小是多少,但期望大小是 20 上下
Lists.newArrayListWithExpectedSize(20);	

newArrayListWithCapacity(20) 方法内部实现是 new ArrayList<>(20),而 newArrayListWithExpectedSize(20) 方法内部实现是对 List 大小有一个计算公式:Ints.saturatedCast(5L + arraySize + (arraySize / 10)),arraySize 表示传进来的值,公式简化下就是 5 + 11/10 * arraySize,因为这个方法表示期望的大小,所以这里取的约是期望值的十分之十一,比传进来的值约大十分之一,所以根据 20 最终计算出来的值是 27。

从 Lists 对 List 初始化进行包装的底层源码来看,底层源码非常简单的,但我们还是愿意使用这种方式的包装,主要是因为这种工厂模式的包装,使我们的使用姿势更加优雅,使用起来更加方便。

2.2 分组和反转排序

除了初始化之外,Lists 还提供了两个比较实用的功能,分组和反转排序功能,示例:

List<String> list = new ArrayList<String>() {{
    add("10");
    add("20");
    add("30");
    add("40");
    add("50");
}};

System.out.println("反转之前:" + JSON.toJSONString(list));
list = Lists.reverse(list);
System.out.println("反转之后:" + JSON.toJSONString(list));

执行结果:

反转之前:["10","20","30","40","50"]
反转之后:["50","40","30","20","10"]

reverse 方法底层实现非常巧妙,底层覆写了 List 原生的 get(index) 方法,会把传进来的 index 进行 (size - 1) - index 的计算,使计算得到的索引位置和 index 位置正好相反,这样我们在 get 时,数组索引位置的 index 已经是相反的位置了,达到了反转排序的效果,其实底层并没有进行反转排序,只是在计算相反的索引位置,通过计算相反的索引位置这样简单的设计,得到了反转排序的效果,非常精妙。

在工作中,我们需要把一个 list 进行切分,然后再把每一份丢给线程池去运行,左后把每份运行的结果汇总,Lists 工具类就提供了一个对 list 进行切分分组的方法,示例:

List<String> list = new ArrayList<String>() {{
    add("10");
    add("20");
    add("30");
    add("40");
    add("50");
}};

System.out.println("分组之前:" + JSON.toJSONString(list));
List<List<String>> list2 = Lists.partition(list, 3);
System.out.println("分组之后:" + JSON.toJSONString(list2));

执行结果:

分组之前:["10","20","30","40","50"]
分组之后:[["10","20","30"],["40","50"]]

partition 方法的第二个参数的意思是,你想让分组后的 List 包含几个元素,这个方法底层实现其实就是 subList 方法。

有一点需要注意的是这两个方法返回的 List 并不是 ArrayList,是自定义的 List,所以对于 ArrayList 的有些功能可能并不支持,使用的时候最好能看下源码,看看底层有无支持。

2.3 小结

Lists 上述的方法大大的方便了我们进行开发,简化了使用姿势,但其内部实现却分厂简单巧妙,比如 reverse 方法可以输出相反排序的 List,但底层并没有实现排序,只是计算了索引位置的相反值而已,这点值的我们学习。

3 Maps

3.1 初始化

Maps 也有初始化 Map 的各种方法,原理和 Lists 类似,使用演示:

Maps.newHashMap();
Maps.newLinkedHashMap();
// 这里 Map 的初始化大小公式和 HashSet 初始化公式类似,HashSet 入参集合初始化 HashMap 时,
// 经典的计算初始大小的公式:取最大值(期望的值/0.75 + 1, 默认值 16)
// newHashMapWithExpectedSize 方法底层也是这么算的初始化大小
Maps.newHashMapWithExpectedSize(20);

Maps.newHashMapWithExpectedSize(int expectedSize) 源码如下:

public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
	return new HashMap<>(capacity(expectedSize));
}
static int capacity(int expectedSize) {
	if (expectedSize < 3) {
		checkNonnegative(expectedSize, "expectedSize");
  		return expectedSize + 1;
	}
	if (expectedSize < Ints.MAX_POWER_OF_TWO) {
  		// This is the calculation used in JDK8 to resize when a putAll
  		// happens; it seems to be the most conservative calculation we
  		// can make.  0.75 is the default load factor.
  		return (int) ((float) expectedSize / 0.75F + 1.0F);
	}
	return Integer.MAX_VALUE; // any large value
}

3.2 difference

Maps 提供了一个特别有趣且使用的方法:difference,此方法的目的是比较两个 Map 的差异,入参就是两个 Map,比较之后能返回四种差异:

  1. 左边 Map 独有的 key;
  2. 右边 Map 独有的 key;
  3. 左右 Map 都有的 key,并且 value 相等;
  4. 左右 Map 都有的 key,但是 value’ 不相等。

演示:

Map<String, String> left = ImmutableMap.of("1", "1", "2", "2", "3", "3");
Map<String, String> right = ImmutableMap.of("2", "2", "3", "3333", "4", "4");

MapDifference<String, String> difference = Maps.difference(left, right);

System.out.println("左边 Map 独有的 key:" + difference.entriesOnlyOnLeft());
System.out.println("右边 Map 独有的 key:" + difference.entriesOnlyOnRight());
System.out.println("左右 Map 都有的 key,并且 value 相等:" + difference.entriesInCommon());
System.out.println("左右 Map 都有的 key,但是 value 不相等:" + difference.entriesDiffering());

执行结果:

左边 Map 独有的 key:{1=1}
右边 Map 独有的 key:{4=4}
左右 Map 都有的 key,并且 value 相等:{2=2}
左右 Map 都有的 key,但是 value 不相等:{3=(3, 3333)}

从这个 demo 我们可以看到此方法的强大威力,我们在工作中经常遇到 Map 或者 List 间差异比较的任务,我们就可以直接使用该方法进行比对,List 可以先转换成 Map。

而且 difference 底层的实现也算是最优的实现了,只需要循环一遍,就可以得到上述四种差异结果,源码如下:

private static <K, V> void doDifference(
		// 左边的 map
        Map<? extends K, ? extends V> left,
        // 右边的 map
        Map<? extends K, ? extends V> right,
        // value 比较器
        Equivalence<? super V> valueEquivalence,
        // key 只在左边 map 出现
        Map<K, V> onlyOnLeft,
        // key 只在右边 map 出现(该 map 内容为 right 所有,
        // ==> Map<K, V> onlyOnRight = new LinkedHashMap<>(right))
        Map<K, V> onlyOnRight,
        // key 在左右 map 都有,并且 value 相等
        Map<K, V> onBoth,
        // key 在左右 map 都有,但是 value 不相等
        Map<K, MapDifference.ValueDifference<V>> differences) {

    // 以左边 map 为基准循环
    for (Entry<? extends K, ? extends V> entry : left.entrySet()) {
        K leftKey = entry.getKey();
        V leftValue = entry.getValue();
        // 如果右边 map 包含左边 map 的 key
        if (right.containsKey(leftKey)) {
        	// 从右边独有 map 移除该 key
            V rightValue = onlyOnRight.remove(leftKey);
            // 如果左右 map 值相等,则加入到 onBoth
            if (valueEquivalence.equivalent(leftValue, rightValue)) {
                onBoth.put(leftKey, leftValue);
            } 
            // 反之则加入到 differences
            else {
                differences.put(leftKey, ValueDifferenceImpl.create(leftValue, rightValue));
            }
        } 
        // 如果右边 map 不包含左边 map 的 key
        else {
        	// 则表示该 key 是左边 map 独有的,加入到 onlyOnLeft 即可
            onlyOnLeft.put(leftKey, leftValue);
        }
    }
}

这是一种比较优秀的,快速比对的算法,可以好好看下上面的源码,然后把这种算法背下来。

Sets 的使用方式和 Lists 和 Maps 很类似。

4 总结

在 Guava 对集合的设计中,有两个大点是非常值得我们学习的:

  1. Lists、Maps 的出现给我们提供了更方便的使用姿势和方法,我们在实际工作中,如果碰到特别繁琐,或者特别难用的 API,我们也可以进行一些包装,使更好用,这个是属于在解决目前的痛点问题上的创新,是非常值得提倡的一件事情,往往可以帮助你拿到更好的绩效。
  2. 如果有人问你,List 或者 Map 高效的差异排序算法,完全可以参考 Maps.diffenerce 的内部实现,该方法只使用了一次循环,就可以得到所有相同或不同结果,这种算法在我们工作中也经常被使用。

------------------------------------- END -------------------------------------

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值