@22221cjp
2016-08-17T15:26:02.000000Z
字数 16087
阅读 3753
Java
Guava是一个非常优秀的Java开发工具包,今年以来就一直尝试将Guava的各种特性应用在工作中,最开始应用的就是Guava的线程部分,总感觉一个线程应该是一个工具包的精华部分,所以最开始也是学习了这一部分,然后是guava cache,这两部分也是最先在工作中使用的,后来一段时间看了集合、函数式编程,但是一直没有使用起来,特别是函数式编程,这一块基本是和集合一起来使用的,没使用起来的原因是当时觉得java的集合类在代码中已经完全满足要求了,一些函数编程中的变换(transform)、过滤(filter)、索引(index)等就没有仔细研究,只是写过一些简单的测试,最近刻意在工作中使用后才发现这些东西真是太好使了。相对于线程、缓存之类的需求,代码中对集合的处理更常见。
学习过程知道一个东西很容易,但是要达到将新的知识变成一种思维方式就比较难了,这需要打破已有的编程习惯,且经过多次重复使用逐渐积累经验后才能达到,这也算一点小小的感悟吧。guava不像Spring、Struts等框架是一种编程思想的实现,它是一种能在编程的过程中各种细节的处理上给出一种更便利、优雅的实现方式。所以guava学习起来并不难,也容易理解。
guava主要包括以下一些功能集:
基础的工具类
- 对应的包:com.google.common.base
- 各种常用的工具类和接口:字符串处理、Suppliers、Functions等
缓存
- guava的缓存实现,非常简单易用的应用内缓存,对应的包:com.google.common.cache
集合
- 对JDK的集合进行了扩展,如:MultiMap、BiMap、MultiSet、ListMultiMap、Table。对应的包:com.google.common.collect
- 防御性编程的不可变集合
- 提供了更多的集合操作方法
事件总线
- 提供了非常松散的发布和订阅方式,同时提供了同步和异步的订阅者执行方式,对应的包:com.google.common.eventbus
哈希
- 提供了更灵活的Hash函数实现方式,对应的包:com.google.common.hash
- 提供了BloomFilter算法的实现,该算法允许有一定错误概率的情况下,以牺牲一定空间作为代价,快速的判断一个value是否在已有的大规模集合中。
IO
- 提供了非常方便的文件操作类,所在包:com.google.common.io
函数式编程
- 一般跟集合一起使用,以Fluent的方式对集合进行操作
并发
- 对JDK的并发进行抽象,提供了可回调的Future。对应的包:com.google.common.util.concurrent
其他的一些功能
- 数学计算的Math包:com.google.common.math
- 反射工具类:com.google.common.reflect
- 原生类型:com.google.common.primitives
- ......
上面的列出的有些是在工作中用到的,有的没有用过,为了稍微的完整性(其实Guava还有其他很多方面,只不过我学习浅薄,还没有接触到),下面分别总结下各方面的使用。
基础的工具类有很多,我经常使用的有如下一些,先总结下经常使用的,没有使用过的,以后慢慢补充吧。
防御性编程通常用来检测方法的参数的正确性,在方法的开始就对参数一定的断言,如果断言失败,就抛出异常,从而导致快速失败,guava中Preconditions类用来完成这样的检测工作。
public class PreconditionsTest {@Testpublic void test1() {try {System.out.println(sqrt(-3));} catch (Exception e) {System.out.println(e.getMessage());}try {System.out.println(sum(null, 3));} catch (Exception e) {System.out.println(e.getMessage());}try {System.out.println(getValue(6));} catch (Exception e) {System.out.println(e.getMessage());}}private int sum(Integer v1, Integer v2) {Preconditions.checkNotNull(v1, "Illegal Argument passed: first parameter is null.");Preconditions.checkNotNull(v2, "Illegal Argument passed: second parameter is null");return v1 + v2;}private double sqrt(double i) {Preconditions.checkArgument(i >= 0, "input value must more than or equal 0: %s", i);return Math.sqrt(i);}public int getValue(int input) {int[] data = {1, 2, 3, 4, 5};Preconditions.checkElementIndex(input, data.length,"Illegal Argument passed: Invalid index.");return data[input];}}
这段代码输出的结果如下:
input value must more than or equal 0: -3.0
Illegal Argument passed: first parameter is null.
Illegal Argument passed: Invalid index. (6) must be less than size (5)
Preconditions类中很多的方法功能都是一样的,第一个参数提供一个断言,第二个参数是断言失败的情况下,抛出的异常信息。可以根据不同的场合选择不同的方法名。个人感觉一个checkArgument就足够了。
字符串处理最常用的操作:split和join。
对应这样的字符串"a , , b ,c"按逗号分隔后结果中可能有空格,且String类的split方法只能返回分隔后的数组。而Guava对split操作提供了更多更清晰语义的方法。
@Testpublic void test4() {List<String> list = Splitter.on(",").omitEmptyStrings().trimResults().splitToList(",,a, b,c,,d,,");System.out.println(list.toString());List<String> list2 = Splitter.on(",").omitEmptyStrings().limit(3).trimResults().splitToList(",,a, b,c,,d,,");System.out.println(list2.toString());}
结果如下:
[a, b, c, d]
[a, b, c,,d,,]
omitEmptyStrings: 忽略split后的空字符串
trimResults: trim一下结果
splitToList: 返回一个List而不是数组
limit: 返回的结果集size限制为3,只split前两个,后面就不split了而作为一个整体作为第三个元素
join可以看出split的反操作,是将一个List或数组用一个分隔符连接起来。当List中有null时,可以选中忽略或其他意义的字符代替。
/*** 演示普通的字符串连接,同时可以看出join方法不具有记忆性*/@Testpublic void test1(){Joiner joiner = Joiner.on(",").skipNulls();String result = joiner.join("a", 1, null, 'c');System.out.println(result);String[] names = new String[]{"zhangsan", "lisi", "wangwu"};System.out.println(joiner.join(names));}
输出结果:
a,1,c
zhangsan,lisi,wangwu
指定出现null替换为其他字符串:
/*** 当出现null时替换*/@Testpublic void test2(){Joiner joiner = Joiner.on(", ").useForNull("NULL");String[] names = new String[]{"zhangsan", "lisi", null, "wangwu"};System.out.println(joiner.join(names));}
输出结果:
zhangsan, lisi, NULL, wangwu
Guava提供了非常方便的Comparator,对于大多数情况下,我们不需要自己实现排序的比较器。guava提供的比较器实例是Ordering类,这个类实现了Comparator接口。
对如下Person类排序作为示例:
class Person {int age;String name;public Person(int age, String name) {this.age = age;this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}}
对Person对象按age和name进行排序:
@Testpublic void test1() {List<Person> list = Lists.newArrayList();list.add(new Person(20, "zhangsan"));list.add(new Person(30, "lisi"));list.add(new Person(19, "wangwu"));//执行顺序,从后向前,先取age,然后将null放到前面,然后自然排序。//自然排序是指,数字按大小,日期按先后,字符串按字典序//onResultOf对一个对象中的某个字段排序//排序是稳定的Ordering<Person> personAgeOrdering = Ordering.natural().nullsFirst().onResultOf(new Function<Person, Integer>() {@Overridepublic Integer apply(Person input) {return input.age;}});Collections.sort(list, personAgeOrdering);System.out.println(JSON.toJSONString(list, true));Collections.sort(list, personAgeOrdering.reverse());//倒序System.out.println(JSON.toJSONString(list, true));//natural 对字符串是按字典序Ordering<Person> personNameOrdering = Ordering.natural().nullsFirst().onResultOf(new Function<Person, String>() {@Overridepublic String apply(Person input) {return input.name;}});list.add(new Person(22, "liayz"));Collections.sort(list,personNameOrdering);System.out.println(JSON.toJSONString(list, true));System.out.println(JSON.toJSONString(personAgeOrdering.greatestOf(list, 2)));System.out.println(JSON.toJSONString(personAgeOrdering.leastOf(list, 2)));}
结果如下:
[{"age":19,"name":"wangwu"},{"age":20,"name":"zhangsan"},{"age":30,"name":"lisi"}]
[{"age":30,"name":"lisi"},{"age":20,"name":"zhangsan"},{"age":19,"name":"wangwu"}]
[{"age":22,"name":"liayz"},{"age":30,"name":"lisi"},{"age":19,"name":"wangwu"},{"age":20,"name":"zhangsan"}]
[{"age":30,"name":"lisi"},{"age":22,"name":"liayz"}]
[{"age":19,"name":"wangwu"},{"age":20,"name":"zhangsan"}]
Ordering类的natural能按:
onResultOf方法对用于对一个类的某个属性排序。如果本身List的元素本身在你期望的字段上是Comparable的,就不需要使用这个方法。reverse方法将比较器取反,按倒序进行排序。注意:reverse返回一个新的Ordering实例,原来的并没有改变。 nullsFirst方法会将列表中的排序字段为null的放到前面。
Ordering实例还有一些其他的方法:
sortedCopy:先拷贝参数中的list,然后做排序
isOrdered:判断一个list是否有序
isStrictlyOrdered:判断是否严格有序,不能有相同元素
max:取一个list中的最大值
min:取一个list中的最小值
greatestOf(list, 2):返回一个list中最大的前2个元素
leastOf(list, 2):返回一个list中最小的2个元素
实际使用中有两种方式都能启动缓存的效果:
先看下简单的Supplier,这个单词英文的意思是:提供者、供应者,这个到底供应啥呢?当然是数据。事实上可以将Supplier看成是简单的轻量级缓存。看如下例子:
@Testpublic void test36() {Supplier<Integer> supplier = new Supplier<Integer>() {@Overridepublic Integer get() {System.out.println("called...");return 4;}};Supplier<Integer> supplier1 = Suppliers.memoizeWithExpiration(supplier, 3, TimeUnit.SECONDS);RateLimiter limiter = RateLimiter.create(1);for (; ; ) {limiter.acquire();//call per secondSystem.out.println(supplier1.get());}}
先看下结果:
called...
4
4
4
called...
4
4
4
4
called...
4
4
4
called...
上面代码通过Suppliers类的静态方法memoizeWithExpiration将一个普通的数据供应者加上了一个缓存,并设置了过期时间为3s。
先来了解下一个新的类:RateLimiter,这个类从名字上看,意思是:速率限制器。这个类也是guava提供的一个有意思的类。通过RateLimiter.create(1)方法设置了一个1s内只释放一个许可的RateLimiter实例。在需要限制速率的地方只需要通过limiter.acquire()获取许可即可。
RateLimiter 还能有一个预热阶段,下面的示例演示了在5s内逐渐将每秒内的许可数量增加到10
/*** 演示速率限制器的预热过程,预热时间是5,单位秒* 运行的效果是由4、6、7、8、10渐渐增加到每s放10个*/@Testpublic void test35() {RateLimiter limiter = RateLimiter.create(10, 5, TimeUnit.SECONDS);DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");for (; ; ) {limiter.acquire();System.out.println(df.format(new Date()));}}
之前在导入晒一晒数据的时候,就使用过速率限制,因为晒一晒的数据是通过Http直接拉线上服务器的数据,然后通过解析JSON格式的数据,再导入到社区的。为了不给线上服务器太大的压力,通过Http拉取数据的时候,就通过这个类设置了速率限制。
在了解了RateLimiter后,再回头看下Supplier,可以看到大概每3s就调用了一次supplier中的get方法,输出:called...,其余的时候只是输出了结果4,而没有实际调用supplier中的方法,相当于起了缓存的作用了。
可以看到Supplier是一种很简单的轻量级的缓存,跟通常意义上的缓存相比,它并不通过key去获取对应的value。非常适合将一个全局的配置数据(loadAll,这些数据不是很大)通过这种方式缓存。
工作中已经多次应用过Supplier缓存数据,比如颜值PK中将颜值PK的全部配置信息(loadAll)通过Supplier缓存,在商户通中将所有模块信息(loadAll)通过这种方式缓存等。
另一种复杂一点的缓存方案就是Guava cache了。缓存就是将一些经常被访问的,少量的数据放到内存中,以加快访问的速度。通过一个单例类将List封装一下就可以实现一个简单的缓存(使用ReadWriteLock实现线程安全),但是要考虑如下一些问题就会使问题变得复杂:
对于这些问题,guava提供非常简单优雅的实现方案。先看一个非常简单实例:
@Testpublic void test6() {CacheLoader<String, String> loader = new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {System.out.println("call..");return key.toUpperCase();}};LoadingCache<String, String> cache = CacheBuilder.newBuilder().build(loader);System.out.println(cache.size());System.out.println(cache.getUnchecked("aaa"));System.out.println(cache.size());System.out.println(cache.getUnchecked("aaa"));try {System.out.println(cache.get("cjp"));} catch (ExecutionException e) {// TODO Auto-generated catch blocke.printStackTrace();}}
该示例中,仅仅将一个类型是字符串作为key,value为它的大写形式。运行的结果如下:
0
call..
AAA
1
AAA
call..
CJP
guava的缓存是一个LoadingCache实例,通过CacheBuilder创建该实例,并传入一个CacheLoader,CacheLoader实例注明了在缓存读取失败时如何加载数据,开始时,缓存中没有任何数据,size为0,当取aaa的时候,触发了缓存加载数据,输出call...,虽然缓存的size变成了1。然后再取aaa时,因为缓存中已经有了该key对应的value,就没有触发加载。
需要注意一下getUnchecked方法和get方法的不同,前者不对可能的异常做检查,调用代码不需要显式的捕捉异常,而后者调用代码需要显式的捕获异常。
这是一个非常简单的示例,可以看到使用guava实现一个缓存非常简单,如果将创建CacheLoader实例和build LoadingCache的两行代码合并,使用仅一行代码就可以实现一个缓存,并且Guava的缓存是线程安全的,可以放心的在多线程的环境中使用。
再看一个稍复杂点的例子:
@Testpublic void test14() throws Exception {//缓存同步删除LoadingCache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).maximumSize(3).recordStats().removalListener(new RemovalListener<String, String>() {@Overridepublic void onRemoval(RemovalNotification<String, String> notification) {System.out.println("remove key[" + notification.getKey() + "],value[" + notification.getValue() + "],remove reason[" + notification.getCause() + "]");System.out.println("remove thread name " + Thread.currentThread().getName());}}).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {System.out.println("key[" + key + "] to upper case");return key.toUpperCase();}});System.out.println(cache.getUnchecked("a"));System.out.println(cache.getUnchecked("b"));System.out.println("thread name " + Thread.currentThread().getName());cache.invalidate("b");//删除key为b的值System.out.println(cache.getUnchecked("a"));Thread.sleep(5000);System.out.println(cache.getUnchecked("c"));System.out.println(cache.getUnchecked("a"));System.out.println(cache.stats().toString());System.out.println("end");}
输出结果如下:
key[a] to upper case
A
key[b] to upper case
B
thread name main
remove key[b],value[B],remove reason[EXPLICIT]
remove thread name main
A
remove key[a],value[A],remove reason[EXPIRED]
remove thread name main
key[c] to upper case
C
key[a] to upper case
A
CacheStats{hitCount=1, missCount=4, loadSuccessCount=4, loadExceptionCount=0, totalLoadTime=3460000, evictionCount=1}
end
这一次创建了一个稍复杂的LoadingCache实例。各方法意义如下:
expireAfterWrite:写入缓存后的过期时间
maximumSize:缓存的最多存放元素个数
recordStats:对缓存命中情况进行统计
removalListener:设置缓存数据失效时监听器
guava中很多地方都是这种fluent的方式,看上去是不是很酷!
在删除的监听器中打印线程的名字是为了显示该监听器是同步的还是异步的。可以看到删除监听是同步的,因为和主线程的名字是一样的,其实可以理解,因为我们并没有指定额外的线程池。删除监听器中可以看到删除的key、value、cause。主线程sleep 5s后,缓存中key为a的元素就过期了,可以看到监听器被调用,最后通过cache.stats()取得缓存命中的情况统计。可以看到命中1次,miss了4次(load了4次),事实上的确如此。
能否设置删除操作是异步的呢?当然可以!通过RemovalListeners.asynchronous方法就可以创建一个异步的listener对象。如下方式创建LoadingCache:
LoadingCache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).maximumSize(3).recordStats().removalListener(RemovalListeners.asynchronous(new RemovalListener<String, String>() {//删除缓存监听器异步删除@Overridepublic void onRemoval(RemovalNotification<String, String> notification) {System.out.println("remove key[" + notification.getKey() + "],value[" + notification.getValue() + "],remove reason[" + notification.getCause() + "]");System.out.println("remove thread name " + Thread.currentThread().getName());}}, Executors.newCachedThreadPool())).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {System.out.println("key[" + key + "] to upper case");return key.toUpperCase();}});
可以看到RemovalListeners.asynchronous方法接受两个参数,第一个参数是RemovalListener对象,第二个参数接收一个线程池,这样就可以异步的设置删除监听器了。如果运行可以看到主线程的线程名和监听器中的线程名是不同的。
上面创建缓存的方式是通过expireAfterWrite指定元素的过期时间,达到重新加载的。也就是说当过期后,这个元素就不存在了,再获取的时候就要通过load重新加载,当加载的时候,获取value的主线程必须同步的等缓存加载完获得数据后才能继续执行。这在一定程度上限制了访问速度。
如果数据量不大的情况下,就不必使用过期时间这种方式,而使用刷新,使用refreshAfterWrite指定刷新的时间间隔。看如下代码:
/*** 测试refresh*/@Testpublic void test16() throws InterruptedException {LoadingCache<String, String> cache = CacheBuilder.newBuilder().recordStats().refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {System.out.println("load key[" + key + "]");return key.toUpperCase();}@Overridepublic ListenableFuture<String> reload(String key, String oldValue) throws Exception {System.out.println("reload key[" + key + "],oldValue[" + oldValue + "]");return super.reload(key, oldValue);}});System.out.println(cache.getUnchecked("a"));System.out.println(cache.getUnchecked("b"));cache.refresh("a");Thread.sleep(3000);System.out.println(cache.getUnchecked("a"));System.out.println(cache.getUnchecked("c"));}
这是一个非常简单的refresh示例,如果使用refreshAfterWrite,需要实现CacheLoader的reload方法,如果不实现,他有一个默认的实现,就是本示例展示的代码,直接调用load方法。代码的运行结果如下:
load key[a]
A
load key[b]
B
reload key[a],oldValue[A]
load key[a]
reload key[a],oldValue[A]
load key[a]
A
load key[c]
C
本例中刷新的时间设置为3s,再第一次显式的调用cache.refresh("a")的时候,可以看到reload方法被调用了。但是reload直接走默认的实现,调用了load方法,所以接着就输出了load key[a]当主线程sleep 3s后,再取a的值时因为超过刷新间隔,又会调用reload方法。可以想象这里的reload肯定是以同步的方式进行的,因为我们并没有指定额外的线程池用来执行reload方法,也就是说当到达刷新时间间隔后,取value的主线程还是要等refresh结束,才能拿到数据后执行,这和刚才的expireAfterWrite方式差不多。如你所想,guava肯定提供了异步刷新的方式,没错!看代码:
/*** 缓存失效时异步重现加载,缓存调用者永远不用阻塞等** @throws InterruptedException*/@Testpublic void test37() throws InterruptedException {LoadingCache<String, String> cache = CacheBuilder.newBuilder().recordStats().refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {System.out.println("load key[" + key + "]");return key.toUpperCase();}@Overridepublic ListenableFuture<String> reload(final String key, String oldValue) throws Exception {ListenableFutureTask<String> task = ListenableFutureTask.create(new Callable<String>() {@Overridepublic String call() throws Exception {System.out.println("reload key[" + key + "] synchronize at thread[" + Thread.currentThread().getName() + "],this will take 1 second...");Thread.sleep(1000);System.out.println("reload end...");return key.toUpperCase();}});Executors.newCachedThreadPool().execute(task);System.out.println("reload key[" + key + "],oldValue[" + oldValue + "]");return task;}});//注意:如果重来没有被get过,在缓存中完全没有,第一次调用会执行load,然后加入到cache中,只有被加入到其中的//到达失效时间后,再被加载的时候才会触发reloadSystem.out.println(cache.getUnchecked("a"));System.out.println(cache.getUnchecked("b"));cache.refresh("a");Thread.sleep(3000);//这里的取a 不会触发reload,因为上面refresh需要耗1s才能结束,而主线程这里只需要等3s//所以这里的a还有1s的存活时间System.out.println(cache.getUnchecked("a"));//但是这里的b 就必须reload了,但是reload的过程需要注意下:先调用load方法,然后发现失效了,但是还会返回之前//缓存中的值,同时会加载reload,因为是异步reload,主线程这里不用等reload结束,继续向下运行获取c的值System.out.println(cache.getUnchecked("b"));System.out.println(cache.getUnchecked("c"));//这里再暂停5s是为了看清楚上面reload b的结束Thread.sleep(5000);}
本示例依然设置refresh时间为3s。重点是reload方法,先打印出reload执行所在的线程名,为了能清楚的看到主线程不需要等refresh完,这里sleep了1s。其他代码跟之前的差不多,运行结果如下:
load key[a]
A
load key[b]
B
reload key[a],oldValue[A]
reload key[a] synchronize at thread[pool-1-thread-1],this will take 1 second...
reload end...
A
reload key[b],oldValue[B]
B
load key[c]
C
reload key[b] synchronize at thread[pool-2-thread-1],this will take 1 second...
reload end...
当执行cache.refresh("a")代码的时候,调用了reload方法,可以看到reload所在线程名是线程池中的。这句代码紧接着主线程sleep了3s,然后又去取a的值,按理说这时候a应该到达了刷新的时间间隔了,但是因为之前的reload方法执行就需要1s,所以对于a来说,还有1s的刷新时间剩余,所以这时取a的值,并不会触发reload。而紧接着取b的值就不同了,因为b没有被refresh过,这时候取b的值达到了刷新的时间间隔,所以会触发reload b。但是因为是异步的刷新,主线程根本不用等刷新完,所以立即输出了原来旧的值B,并立即输出了load c的结果,然后才看到 reload b的过程在继续进行,直到结束。
完美!异步刷新,主线程永远不用等缓存的加载!现在在工作中所有使用Guava cahe的地方全部采用这种方式。
注意如下几点:
refreshAfterWrite和expireAfterWrite的区别
- refreshAfterWrite只不过在刷新时间间隔到的时候,调用reload方法获取对于的key对于的value后替换当前内存中的key值。原来内存中的key对于的value是一直存在的。
- expireAfterWrite方式当达到过期时间后,内存中的对应的key-value就被删除了(应该是被动删除方式,其实还在内存中,获取key的瞬间被删除)。只能通过load方法重新加载key对于的value。
refresh方式并不是达到时间间隔后就立即刷新,而是在get数据的时候,发现超过刷新时间间隔了才会刷新,是被动的方式。
guava cache在工作中已经被多次使用了。因为是本地缓存,在分布式环境下,使用时需要注意一下情况:
- 读操作: 可以直接根据key从guava中取,根本不用担心取不到的情况。
- 写操作: 直接写入数据库,不用同步的写缓存,分布式环境下也能正常工作,不会出现数据一致性问题。
- 更新: 一台机器的更新不能实时的反应到集群中其他机器上,会出现数据不一致问题。
- 删除: 类似更新。
所以guava cache并不太适合更新频繁且对数据实时性要求较高的场景。我一般用来缓存一些配置信息,结合spy清理缓存,能够做到配置信息修改,能够立即起到效果。颜值PK中的配置信息就是这么应用的,因为活动的不同状态,运营经常改规则、头图等信息,就使用了Guava cache结合spy让配置信息及时生效。
对guava提供的两种缓存方案灵活运用,在开发中能够很方便的搭建本地缓存。接下来看看集合。
Guava对JDK的集合做了扩充,主要表现在:
在看Guava提供的集合类的时候,又看了下JDK提供的集合类,有个特点,guava提供的集合类很多和JDK提供的集合类在继承的层次和实现方式上很多是对应的。

