Google-Guava包学习

很久以前写项目的时候, 就发现许多工具类其实都是在重复造轮, 而且还造的不好 (因为基本也就是常规思路, 很难有什么神来之笔的改观) , 大多是能跑通就凑合着用了.

而在读源码的过程中, 就会发现很多优秀设计和经典库的使用, 这次学习的就是大名鼎鼎且又是Google出的Guava 库, Github多达3w+star.. 先还是选最常用的模块说, 不求全, 但求精. (Guava源码质量也很高, 之后会深入拜读一下)

0x00. 前言

因为Guava 的文章很多, 源码分析也不在少数, 大众化一查既有的就不多重复了, 就给个简要的说明 + 文档链接参考 ~ (优先官方文档)

这里单独写它关键在于记录一下自己日常遇到/用到, 且使用原生库很不好解决的一些点, 然后看看使用了Guava 之后是否就会好许多, 以及对比一下具体的实现. (而不是只说Guava 怎么好, 怎么用, 原生怎么臃肿, 其实JDK8~JDK11也优化了很多的地方, 很多文章的观点也是过时的了..)

先大致列一下Guava的几个核心模块:

  • 基本工具 (避空, Object方法重写..)
  • 集合 (主要是提供了许多更全面的综合型数据结构, 以及更好的处理)
  • 缓存 (快速实现本地缓存, 过期策略等)
  • 并发 (异步回调和抽象的服务框架)
  • 字符串处理 (内有无符号实现, 扩展)
  • 其他 (…)

完整的Guava 有十几个模块, 但是许多不一定使用, 且JDK已经吸收采纳了很多, 建议原生已支持的, 优先使用原生方法, 更易于维护, 然后这里挑几个重点, 很推荐使用的来说说. (目前只是简单说说用法, 源码分析和具体对比慢慢补 , 见谅….)

0x01. 字符串

字符串的处理优化我想是最常用的操作之一, 即使JDK在不断更新相关的类, 也不妨碍学习其他途径来优化使用它, 并且这块内容不多, 我觉得作为入门接触比较合适, 主要有5个类 (处理字符串的切割, 组合, 字符匹配处理等) ,由于JDK8已经内置了不少Join 相关的类, 我觉得拼接部分可以先不看. 先来看看切割

1.切割

官方文档中可以看到介绍一个很关键功能上的问题, 字符串尾部的空字符会被忽略(why?). 首先有两个事实:

  1. 早些时候的Java原生的split() 核心使用的正则匹配而不是KMP算法, 导致以前效率很低, 大家都提出了各种替代方案和自造轮子的方案, 参考图文:

    javaSplit00

  2. 但是, JDK8split() 对单字符的匹配做了专门的预处理优化, 具体优化效果如何呢? 先看看源码截取:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public String[] split(String regex, int limit) { //默认limit参数是0, 设计的时候就会去末尾空字符..
    /* 满足以下任一条件,则进入fastpath模式
    (1)单字符: this character is not one of the RegEx's meta characters ".$|()[{^?*+\\"
    (2)双字符: first char is the '\' and the second is not the ascii digit or ascii letter.
    */
    char ch = 0;
    if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
    (regex.length() == 2 && regex.charAt(0) == '\\' &&
    (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
    ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) &&
    (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE))
    {..core code ,use indexOf() & substing() rather than regex-model..}
    }

    所以可以看出, 在新版的JDK8里, split() 在单字符的时候已经调整为了上面图里的indexOf+substring 循环判断的方式了, 并不存在上面^(1)^的性能问题, 请大家不要以讹传讹 , 9012年看到一个结论, 还是先看看源码 + 写个demo测试一下, 不要听风就是雨了… 不过, 性能上是OK了, 但是发现功能上的缺陷还是存在 ↓

如同Guava 官方文档里说的一样, 原生的split()莫名会自动去掉字符串尾部的空字符, 导致存在隐患 (当然这个也是有原因并可以解决的, 最简单把另一个limit参数设置为-1 ,参考split完整参数)

Quiz: What does ",a,,b,".split(",") return?

Expected: “”, “a”, “”, “b”, “” (5 str) // ",a,,b,".split("," , -1)is right ..

Actually: “”, “a”, “”, “b” (4 str)

但是不管是低版本JDK存在的性能问题, 还是大家很容易忽视或随便设置一个负数的功能问题, 如果项目已经引入了Guava 包, 那还是推荐使用它的Splitter 类, 没有以上后顾之忧的同时, 还做了不少其他性能&功能的优化, 常用方式 :

1
2
//Char
Splitter.on(',').splitToList(originStr);

其他参考官方文档,

补充: Splitter还有个同胞Joiner , 用来做字符串各种拼接操作, 但是由于JDK8已经在stream 和原生库自带, 建议优先使用原生库.

0x02. 集合

集合是Guava 被使用最广泛且难以被原生替代的模块, 里面有大量优雅实用的结构设计, 所以第二个接触的就是集合(Collections) 模块. 同样, 由浅入深 ,我们这里分数据结构和集合工具类两块来说:

A,数据结构

1. 不可变集合

主要说的是常见的list/set/map ,这里先直观的给出几个例子, 对比一下使用Guava 和原始的写法区别:

1
2
3
4
5
6
7
8
9
10
//许多场景,尤其是单元测试里,我们经常需要list/set/map. 原始写法
List<T> list = new ArrayList<>() {{
add("a");
add("b");
add("c");
}}

//使用guava,简洁.同理Immutable还有其他几个常用的集合朋友
//如果需要可变的集合,那直接使用工具类对应的静态工厂方法就行(见后)
ImmutableList<T> of = ImmutableList.of("a", "b", "c");

其实很多时候大家可能不习惯用不可变的对象(包括final的各种用法) , 但实际上很多场景下集合都应该也可以是不可变的, 可自行查阅原因和优点.

虽然JDK原生也有Collections.unmodifiableXxx 的方法, 但是实现并不好, 所以学习了Immutable 家族后, 可以尽可能多的替代之前的写法了, 安全性和效率都要高不少.

2. BiMap (双向)

先回想一下链表和双向链表的区别, 再想一下普通的Map的映射方式, 就有以下两个问题:

  1. 什么时候需要双向的Map (K<–>V相互能映射)?
  2. 怎么设计双向Map?

3. Table (表)

这也是Guava中一个新设计的重要数据结构, 因为结构灵活, 查询效率高, 使用方法也很简单, 先看看它的声明定义:

1
2
//估计一下是什么结构? R,C,V各代表什么
Table<R, C, V> tables = HashBasedTable.create();

那它到底是怎么用的呢? 简单来说, 类似我们在mysql 中常见的表结构, 由“行+列” 二者同时确定一个元素, 它对应已有的数据结构应该是

1
2
3
4
5
6
7
8
//还原到原生数据结构
Table<Row, Column, Value> <--> Map<Row, Map<Column, Value>>

//再来个很简单的初始化, 就能明白它的用途了 (map通过K确定V, tables再加了一层映射)
tables.put("1", "A", 7);
tables.put("1", "B", 77);
tables.put("2", "A", 777);
tables.put("2", "B", 7777);

B. 工具类

JDK里大家耳熟能详的是Collections 这个集合工具类, 提供了一些基础的集合处理转换功能, 但是实际使用里很多需求并不是简单的排序, 或者比较数值大小, 然后Guava在此基础上做了许多的改进优化, 可以说是Guava最为成熟/流行的模块之一, 那到底有哪些很好用的工具呢?

1. Lists

基本上guava里增强的工具类名字都是数据结构名+s (比如Lists), 那先看看上面想初始化一个可变集合, 应该怎么做?

1
2
3
4
List<String> list = Lists.newArrayList("x","y","z","n","m"); //注意:并非所有数据结构都能直接这样传入值

//剩下的工具方法主要就是切分和反转. 可能用到切分多一些
List<List> lists = Lists.partition(list, 2); // output --> {{x,y}, {z,n}, {m}}

2. Sets

不同于Lists提供的主要都在做初始化操作的方法, Sets中提供了几个比较实际一点的取“并/交”集的方法, 不过总体还是比较单薄, 这里主要是想说, 大家使用的时候建议看看源码里的算法和实现方式, 比如Apache Common 3/4 和Guava 和你自己以前写的实现可能就都不一样, 但是性能上可能有几十倍的差别. 不妨好好对比一下, 会对基本功提高有不少帮助.

3. Maps

Maps 提供的工具方法应该算是几个集合工具类里最有特色也最多的,

0x03. 并发

众所周知的是JDK自带了一个concurrent 并发库, 不仅对常见的数据结构实现了专门的并发版 , 还提供了一个很著名的Future接口, 那么Guava 里在这一块又做了什么呢? 先来回顾一下JDK原生的Future来源运行机制, 这对理解后面内容非常必要.

首先 , 有几个名词需要稍微区分一下, 以免混淆了概念: (进/线程就不说了)

  • 多线程 : 假设把吃完饭菜当做要完成的任务 , 为了更快的吃完, 开多线程 = 让多个人在一张桌子(单进程)上吃饭, 效率会高许多, 缺点是当大家同时吃一道菜的时候, 容易发生”夹菜转盘, 伸筷无菜“的资源竞争问题.

  • 并发 : 在一个时间段内, 有处理多任务的能力. 比如5盘菜, 我能每盘菜都吃一口, 最后把它们吃光, 而不是每盘菜吃完才能去吃下一盘, 就可以说是并发了, 而常见PL中实现并发的方式就是 A. 多线程 B. 异步

    ( PS : 补充一个更形象的例子, 有些场所为了节省成本, 提高资源利用率, 会有男女混用厕所, 这其实就满足了并发的要求.. 而并行就是常见的男女厕所分开, 那就容易出现景点女厕排长队但男厕很空闲的情况 ~ )

  • 并行 : 在一个时间点, 同时处理不同任务. 比如我一边吃饭的时候, 一边接听电话, 还一边在看电视….. 关键是强调同一时间 , 硬性要求更高(最常见就是多核CPU), 所以我们编程中一般更关注的是“并发”

  • 异步 : 对立面是”同步”, 异步和非阻塞虽然常常搭配出现, 但是没有必然联系

  • 阻塞 : 其实这个场景是最常见的, 举个例子你去医院排队挂号缴费, 就是典型的阻塞, 中间如果有100个人, 你中途走了就得重新排, 非常浪费时间, 你想如果能估计一下大概等待的时间, 去做别的事就好了, 那就是后面出现的”叫号机”了, 它也可以看做是一种异步的实现.

  • 回调 : 这个常与异步搭配使用, 形象的比喻就是你淘宝看中了一款衣服, 但是缺货, 你设置了到货提醒, 等衣服有货了, 它就会自动触发“发短信”的方式来回调通知你.

那把上面的基础概念理一下之后, 我们就知道并发的本质, 就是关注Java中多线程异步的具体实现了, 它们本质上并无关联, 但在各自适合的场景下都能发挥出很好的并发效果, 所以很多人并没有完全搞清楚区别和用法, 这里先说模型简单一点的异步:

如今, 大家可能更熟悉的PL代表是Node.js —-类单线程异步非阻塞的模型 , 但是这个组合词还是比较拗口, 不妨先想想实际的例子:

任务描述: 小明同学要把1万个苹果从山上丢给山下的汤姆. 在汤姆能够正常接收到的前提下, 要求尽可能的完成

思路一: 小明同学请50个人, 一起从山上丢苹果, 但发现山下的汤姆一个人接不来, 需要在山下也请50人帮忙接苹果

思路二: 小明同学买一台自动传输苹果并带反馈显示的机器, 他不再丢一个苹果等汤姆响应是否接到了, 而是不停的把苹果丢到传输机上, 让传输机送往山下汤姆处, 汤姆每收到一定的苹果就给机器一个反馈, 然后小明可以在山顶看到.

思考一下二者的优劣势? 再想想, 如果把二者结合一下, 会是更好的方式么?

未完待续…

多线程核心是关注调度分配和安全, 那这部分逻辑比较复杂, 引用一张美团技术Blog里分类的很清晰的图 :

javaMThread00

最后注意OS中的相关概念, 同样的名词, PL和OS实现可能有许多区别的, 切不能想当然的推导.

来个实际的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    //给个例子
ListenableFuture<T> future = ListeningExecutorService.submit(Callable<T> task)
Futures.addCallback(future, new FutureCallback<Integer>() {

@Override
public void onSuccess(Integer count) {
doSomething();
asynOperation("Count =", count);
}

@Override
public void onFailure(Throwable t) {
doSomething2(batch);
}
})

需要特别注意的是, JDK1.8中单独添加了CompletableFuture类, 很大的扩展了Future 的功能, 后续应该尽可能多的使用它代替Guava中的工具类

未完待续…..


参考资料:
  1. Guava官方Wiki
  2. Guava-回调之ListeningExecutorService
  3. 个人心得之并发编程书解读
  4. (待补)Thrift到网络编程之多线程
  5. (待补)Thrift到网络编程之异步和NIO