google guava中有cache包,此包提供内存缓存功能。内存缓存需要考虑很多问题,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等等。 当然这些东西guava都考虑到了。
guava中使用缓存需要先声明一个CacheBuilder对象,并设置缓存的相关参数,然后调用其build方法获得一个Cache接口的实例。请看下面的代码和注释,注意在注释中指定了Cache的各个参数。
public static void main(String[] args) throws ExecutionException, InterruptedException{ //缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存 LoadingCache<Integer,Student> studentCache //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例 = CacheBuilder.newBuilder() //设置并发级别为8,并发级别是指可以同时写缓存的线程数 .concurrencyLevel(8) //设置写缓存后8秒钟过期 .expireAfterWrite(8, TimeUnit.SECONDS) //设置缓存容器的初始容量为10 .initialCapacity(10) //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项 .maximumSize(100) //设置要统计缓存的命中率 .recordStats() //设置缓存的移除通知 .removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause()); } }) //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存 .build( new CacheLoader<Integer, Student>() { @Override public Student load(Integer key) throws Exception { System.out.println("load student " + key); Student student = new Student(); student.setId(key); student.setName("name " + key); return student; } } ); for (int i=0;i<20;i++) { //从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据 Student student = studentCache.get(1); System.out.println(student); //休眠1秒 TimeUnit.SECONDS.sleep(1); } System.out.println("cache stats:"); //最后打印缓存的命中率等 情况 System.out.println(studentCache.stats().toString()); }
以上程序的输出如下:
load student 1
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
1 was removed, cause is EXPIRED
load student 1
......
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
Student{id=1, name=name 1}
cache stats:
CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1348802, evictionCount=2}
看看到在20此循环中命中次数是17次,未命中3次,这是因为我们设定缓存的过期时间是写入后的8秒,所以20秒内会失效两次,另外第一次获取时缓存中也是没有值的,所以才会未命中3次,其他则命中。
guava的内存缓存非常强大,可以设置各种选项,而且很轻量,使用方便。另外还提供了下面一些方法,来方便各种需要:
ImmutableMap<K, V> getAllPresent(Iterable<?> keys)
一次获得多个键的缓存值put
和putAll
方法向缓存中添加一个或者多个缓存项invalidate
和invalidateAll
方法从缓存中移除缓存项asMap()
方法获得缓存数据的ConcurrentMap<K, V>
快照cleanUp()
清空缓存refresh(Key)
刷新缓存,即重新取缓存数据,更新缓存
guava缓存过期时间分为两种,一种是从写入时开始计时,一种是从最后访问时间开始计时,而且guava缓存的过期时间是设置到整个一组缓存上的;这和EHCache,redis,memcached等不同,这些缓存系统设置都将缓存时间设置到了单个缓存上。
guava缓存设计成了一组对象一个缓存实例,这样做的好处是一组对象设置一组缓存策略,你可以根据不同的业务来设置不同的缓存策略,包括弱引用,软引用,过期时间,最大项数等。另外一点好处是你可以根据不同的组来统计缓存的命中率,这样更有意义一些。
这样做也是有缺点的,缺点是首先是每个缓存组都需要声明不同的缓存实例,具体到业务程序中可能就是每个业务对象一个缓存了。这样就把不同的业务缓存分散到不同的业务系统中了,不太好管理。
Guava Cache 创建
基本上可以通过两种方式来创建cache:
cacheLoader
callable callback
通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于,这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值。
但不同的在于cacheloader的定义比较宽泛,是针对整个cache定义的,可以认为是统一的根据key值load value的方法。
而callable的方式较为灵活,允许你在get的时候指定。
下面是两种方法的例子:
首先是基于cacheloader的方法
@Test |
public void testCacheBuilder() throws ExecutionException { |
LoadingCache<String, String> graphs = CacheBuilder.newBuilder() |
.maximumSize( 1000 ) |
.build( new CacheLoader<String, String>() { |
public String load(String key) { |
// 这里是key根据实际去取值的方法 |
return "value" ; |
} |
}); |
String resultVal = graphs.get( "testKey" ); |
System.out.println(resultVal); |
} |
其次是基于实现callable的方法:
@Test |
public void testCallable() throws ExecutionException { |
// 没有使用CacheLoader |
Cache<String, String> cache = CacheBuilder.newBuilder() |
.maximumSize( 1000 ) |
.build(); |
String resultVal = cache.get( "testKey" , new Callable<String>() { |
public String call() { |
// 这里先根据key实际去取值的方法 |
return "value" ; |
} |
}); |
System.out.println(resultVal); |
}<span></span> |
被动移除数据的方式,guava默认提供了三种方式:
基于大小的移除
看字面意思就知道就是按照缓存的大小来移除,如果即将到达指定的大小,那就会把不常用的键值对从cache中移除。
定义的方式一般为 CacheBuilder.maximumSize(long),官方还介绍了一种可以算权重的方法,个人认为实际使用中不太用到,暂不讨论。
就这个常用的来看有几个注意点,
其一,这个size指的是cache中的条目数,不是内存大小或是其他;
其二,并不是完全到了指定的size系统才开始移除不常用的数据的,而是接近这个size的时候系统就会开始做移除的动作;
其三,如果一个键值对已经从缓存中被移除了,你再次请求访问的时候,如果cachebuild是使用cacheloader方式的,那依然还是会从cacheloader中再取一次值,如果这样还没有,就会抛出异常
基于时间的移除
guava提供了两个基于时间移除的方法
expireAfterAccess(long, TimeUnit) 这个方法是根据某个键值对最后一次访问之后多少时间后移除
expireAfterWrite(long, TimeUnit) 这个方法是根据某个键值对被创建或值被替换后多少时间移除
基于引用的移除
这种移除方式主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除,个人对垃圾回收这块不是非常了解,窃以为不太可靠。。也不常用。。所以没有研究,欢迎补充。
主动移除数据方式
主动移除有三种方法:
单独移除用 Cache.invalidate(key)
批量移除用 Cache.invalidateAll(keys)
移除所有用 Cache.invalidateAll()
如果需要在移除数据的时候有所动作还可以定义Removal Listener,但是有点需要注意的是默认Removal Listener中的行为是和移除动作同步执行的,如果需要改成异步形式,可以考虑使用RemovalListeners.asynchronous(RemovalListener, Executor)
Guava Cache 的清空,刷新及统计功能
主要介绍guava cache的清空,刷新和统计的功能。
缓存数据的清空
guava没有提供自动触发清空缓存数据的功能,而是提供了一种手工调用的方式,使用者需要通过Cache.cleanUp()的方式来清空缓存。
所以一般可以有两种选择,一种是通过某个请求来触发清空动作,这种相当于按需清空,另一种则是通过定时任务,亦成为调度程序来清空,这种相当于与按时清空
缓存数据的刷新
guava没有提供类似refreshall的方法刷新缓存中的所有值,而只是提供了 LoadingCache.refresh(K)方法,用于刷新某个键值对,这里有趣的是刷新动作是异步的,也就是在值被彻底刷新之前,如果有人取这个key的值,返回的还是没有刷新的值。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
.maximumSize( 1000 ) |
.refreshAfterWrite( 1 , TimeUnit.MINUTES) |
.build( |
new CacheLoader<Key, Graph>() { |
public Graph load(Key key) { // no checked exception |
return getGraphFromDatabase(key); |
} |
public ListenableFuture<Graph> reload( final Key key, Graph prevGraph) { |
if (neverNeedsRefresh(key)) { |
return Futures.immediateFuture(prevGraph); |
} else { |
// asynchronous! |
return ListenableFutureTask.create( new Callable<Graph>() { |
public Graph call() { |
return getGraphFromDatabase(key); |
} |
}); |
} |
} |
}); |
缓存数据统计
可以通过 CacheBuilder.recordStats()方法打开统计, Cache.stats()方法会返回一个CacheStats对象,里面有缓存条目访问率等数据,如果你的缓存需要做一些调优,可以参考这里的数据。
google guava的不可变集合
不可变集合的意义
不可变对象有很多优点,包括:
- 当对象被不可信的库调用时,不可变形式是安全的;
- 不可变对象被多个线程调用时,不存在竞态条件问题
- 不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
- 不可变对象因为有固定不变,可以作为常量来安全使用。
创建对象的不可变拷贝是一项很好的防御性编程技巧。Guava为所有JDK标准集合类型和Guava新集合类型都提供了简单易用的不可变版本。
JDK也提供了Collections.unmodifiableXXX方法把集合包装为不可变形式,但我们认为不够好:
- 笨重而且累赘:不能舒适地用在所有想做防御性拷贝的场景;
- 不安全:要保证没人通过原集合的引用进行修改,返回的集合才是事实上不可变的;
- 低效:包装过的集合仍然保有可变集合的开销,比如并发修改的检查、散列表的额外空间,等等。
如果你没有修改某个集合的需求,或者希望某个集合保持不变时,把它防御性地拷贝到不可变集合是个很好的实践。
重要提示:所有Guava不可变集合的实现都不接受null值。我们对Google内部的代码库做过详细研究,发现只有5%的情况需要在集合中允许null元素,剩下的95%场景都是遇到null值就快速失败。如果你需要在不可变集合中使用null,请使用JDK中的Collections.unmodifiableXXX方法。更多细节建议请参考“使用和避免null”。
如何使用guava的不可变集合
1. 如何创建不可变集合
第一种方法使用builder创建:
public class ImmutableDemo { public static void main(String[] args) { Set<String> immutableNamedColors = ImmutableSet.<String>builder() .add("red", "green","black","white","grey") .build(); //immutableNamedColors.add("abc"); for (String color : immutableNamedColors) { System.out.println(color); } } }
第二种方法使用of静态方法创建:
ImmutableSet.of("red","green","black","white","grey");
第三种方法使用copyOf静态方法创建:
ImmutableSet.copyOf(new String[]{"red","green","black","white","grey"});
2. 使用asList()获得不可变集合的list视图
asList方法是在ImmutableCollection中定义,而所有的不可变集合都会从ImmutableCollection继承,所以所有的不可变集合都会有asList()方法返回当前不可变集合的list视图,这个视图也是不可变的。
3. 不可变集合的使用
不可变集合的使用和普通集合一样,只是不能使用他们的add,remove等修改集合的方法。
google guava集合之Multiset
Multiset是什么?
Multiset看似是一个Set,但是实质上它不是一个Set,它没有继承Set接口,它继承的是Collection<E>接口,你可以向Multiset中添加重复的元素,Multiset会对添加的元素做一个计数。
它本质上是一个Set加一个元素计数器。
Multiset使用示例:
package cn.outofmemory.guava.collection; import com.google.common.base.Splitter; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; public class MultisetDemo { public static void main(String[] args) { Multiset multiset = HashMultiset.create(); String sentences = "this is a story, there is a good girl in the story."; Iterable<String> words = Splitter.onPattern("[^a-z]{1,}").omitEmptyStrings().trimResults().split(sentences); for (String word : words) { multiset.add(word); } for (Object element : multiset.elementSet()) { System.out.println((String)element + ":" + multiset.count(element)); } } }
在上面的示例中我们对一段文字拆分成一个一个的单词,然后依次放入到multiset中,注意这段文字中有多个重复的单词,然后我们通过for循环遍历multiset中的每一个元素,并输出他们的计数。输出内容如下:
story:2
is:2
girl:1
there:1
a:2
good:1
the:1
in:1
this:1
显然计数不是问题,Multiset还提供了add和remove的重载方法,可以在add或这remove的同时指定计数的值。
常用实现 Multiset 接口的类有:
- HashMultiset: 元素存放于 HashMap
- LinkedHashMultiset: 元素存放于 LinkedHashMap,即元素的排列顺序由第一次放入的顺序决定
- TreeMultiset:
元素被排序存放于
TreeMap - EnumMultiset: 元素必须是 enum 类型
- ImmutableMultiset: 不可修改的 Mutiset
看到这里你可能已经发现 Guava Collections 都是以 create 或是 of 这样的静态方法来构造对象。这是因为这些集合类大多有多个参数的私有构造方法,由于参数数目很多,客户代码程序员使用起来就很不方便。而且以这种方式可以返回原类型的子类型对象。另外,对于创建范型对象来讲,这种方式更加简洁。
google guava中定义的String操作
在google guava中为字符串操作提供了很大的便利,有老牌的判断字符串是否为空字符串或者为null,用指定字符填充字符串,以及拆分合并字符串,字符串匹配的判断等等。
下面我们逐一了解这些操作:
1. 使用com.google.common.base.Strings类的isNullOrEmpty(input)方法判断字符串是否为空
//Strings.isNullOrEmpty(input) demo
String input = "";
boolean isNullOrEmpty = Strings.isNullOrEmpty(input);
System.out.println("input " + (isNullOrEmpty?"is":"is not") + " null or empty.");
2. 获得两个字符串相同的前缀或者后缀
//Strings.commonPrefix(a,b) demo String a = "com.jd.coo.Hello"; String b = "com.jd.coo.Hi"; String ourCommonPrefix = Strings.commonPrefix(a,b); System.out.println("a,b common prefix is " + ourCommonPrefix); //Strings.commonSuffix(a,b) demo String c = "com.google.Hello"; String d = "com.jd.Hello"; String ourSuffix = Strings.commonSuffix(c,d); System.out.println("c,d common suffix is " + ourSuffix);
3. Strings的padStart和padEnd方法来补全字符串
int minLength = 4; String padEndResult = Strings.padEnd("123", minLength, '0'); System.out.println("padEndResult is " + padEndResult); String padStartResult = Strings.padStart("1", 2, '0'); System.out.println("padStartResult is " + padStartResult);
4. 使用Splitter类来拆分字符串
Splitter类可以方便的根据正则表达式来拆分字符串,可以去掉拆分结果中的空串,可以对拆分后的字串做trim操作,还可以做二次拆分。
我们先看一个基本的拆分例子:
Iterable<String> splitResults = Splitter.onPattern("[,,]{1,}") .trimResults() .omitEmptyStrings() .split("hello,word,,世界,水平"); for (String item : splitResults) { System.out.println(item); }
Splitter的onPattern方法传入的是一个正则表达式,其后紧跟的trimResults()方法表示要对结果做trim,omitEmptyStrings()表示忽略空字符串,split方法会执行拆分操作。
split返回的结果为Iterable<String>,我们可以使用for循环语句来逐个打印拆分字符串的结果。
Splitter还有更强大的功能,做二次拆分,这里二次拆分的意思是拆分两次,例如我们可以将a=b;c=d这样的字符串拆分成一个Map<String,String>。
String toSplitString = "a=b;c=d,e=f"; Map<String,String> kvs = Splitter.onPattern("[,;]{1,}").withKeyValueSeparator('=').split(toSplitString); for (Map.Entry<String,String> entry : kvs.entrySet()) { System.out.println(String.format("%s=%s", entry.getKey(),entry.getValue())); }
二次拆分首先是使用onPattern做第一次的拆分,然后再通过withKeyValueSeperator('')方法做第二次的拆分。
5. 有拆分字符串必然就有合并字符串,guava为我们提供了Joiner类来做字符串的合并
我们先看一个简单的示例:
String joinResult = Joiner.on(" ").join(new String[]{"hello","world"}); System.out.println(joinResult);
上面例子中我们使用Joiner.on(" ").join(xx)来合并字符串。很简单也很有效。
Splitter方法可以对字符串做二次的拆分,对应的Joiner也可以逆向操作,将Map<String,String>做合并。我们看下下面的例子:
Map<String,String> map = new HashMap<String,String>(); map.put("a", "b"); map.put("c","d"); String mapJoinResult = Joiner.on(",").withKeyValueSeparator("=").join(kvs); System.out.println(mapJoinResult);
使用withKeyValueSeparator方法可以对map做合并。合并的结果是:a=b,c=d
guava库中还可以对字符串做大小写转换(CaseFormat枚举),可以对字符串做模式匹配。使用起来都很方便,就不一一介绍了。
google guava中的对象操作封装
我们在开发中经常会需要比较两个对象是否相等,这时候我们需要考虑比较的两个对象是否为null,然后再调用equals方法来比较是否相等,google guava库的com.google.common.base.Objects类提供了一个静态方法equals可以避免我们自己做是否为空的判断,示例如下:
Object a = null; Object b = new Object(); boolean aEqualsB = Objects.equal(a, b);
Objects.equals的实现是很完美的,其实现代码如下:
public static boolean equal(@Nullable Object a, @Nullable Object b) { return a == b || (a != null && a.equals(b)); }
首先判断a b是否是同一个对象,如果是同一对象,那么直接返回相等,如果不是同一对象再判断a不为null并且a.equals(b). 这样做既考虑了性能也考虑了null空指针的问题。
另外Objects类中还为我们提供了方便的重写toString()方法的机制,我们通过例子来了解一下吧:
package cn.outofmemory.guava.base; import com.google.common.base.Objects; public class ObjectsDemo { public static void main(String [] args) { Student jim = new Student(); jim.setId(1); jim.setName("Jim"); jim.setAge(13); System.out.println(jim.toString()); } public static class Student { private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return Objects.toStringHelper(this.getClass()) .add("id", id) .add("name", name) .add("age", age) .omitNullValues().toString(); } } }
我们定义了一个Student类,该类有三个属性,分别为id,name,age,我们重写了toString()方法,在这个方法中我们使用了Objects.toStringHelper方法,首先指定toString的类,然后依次add属性名称和属性值,可以使用omitNullValues()方法来指定忽略空值,最后调用其toString()方法,就可以得到一个格式很好的toString实现了。
上面代码输出的结果是:
Student{id=1, name=Jim, age=13}
这种方式写起来很简单,可读性也很好,所以用Guava吧。
google guava的Preconditions使用
google guava的base包中提供的Preconditions类用来方便的做参数的校验,他主要提供如下方法:
- checkArgument 接受一个boolean类型的参数和一个可选的errorMsg参数,这个方法用来判断参数是否符合某种条件,符合什么条件google guava不关心,在不符合条件时会抛出IllegalArgumentException异常
- checkState 和checkArgument参数和实现基本相同,从字面意思上我们也可以知道这个方法是用来判断状态是否正确的,如果状态不正确会抛出IllegalStateException异常
- checkNotNull方法用来判断参数是否不是null,如果为null则会抛出NullPointerException空指针异常
- checkElementIndex方法用来判断用户传入的数组下标或者list索引位置,是否是合法的,如果不合法会抛出IndexOutOfBoundsException
- checkPositionIndexes方法的作用和checkElementIndex方法相似,只是此方法的索引范围是从0到size包括size,而上面的方法不包括size。
下面我们看一个具体的使用示例:
package cn.outofmemory.guava.base; import com.google.common.base.Preconditions; public class PreconditionsDemo { public static void main(String[] args) { PreconditionsDemo demo = new PreconditionsDemo(); demo.doSomething("Jim", 19, "hello world, hello java"); } public void doSomething(String name, int age, String desc) { Preconditions.checkNotNull(name, "name may not be null"); Preconditions.checkArgument(age >= 18 && age < 99, "age must in range (18,99)"); Preconditions.checkArgument(desc !=null && desc.length() < 10, "desc too long, max length is ", 10); //do things } }
上面例子中的doSomething()方法的开头我们调用了三次Preconditions的方法,来对参数做校验。
看似Preconditions实现很简单,他的意义在于为我们提供了同一的参数校验,并对不同的异常情况抛出合适类型的异常,并对异常信息做格式化。
使用google guava的Optional接口来避免空指针错误
null会带来很多问题,从开始有null开始有无数程序栽在null的手里,null的含义是不清晰的,检查null在大多数情况下是不得不做的,而我们又在很多时候忘记了对null做检查,在我们的产品真正投入使用的时候,空指针异常出现了,这是一种讨厌的情况。
鉴于此google的guava库中提供了Optional接口来使null快速失败,即在可能为null的对象上做了一层封装,在使用Optional静态方法of时,如果传入的参数为null就抛出NullPointerException异常。
我们看一个实际的例子:
package cn.outofmemory.guava.base; import com.google.common.base.Optional; public class OptionalDemo { public static void main(String[] args) { Optional<Student> possibleNull = Optional.of(null); possibleNull.get(); } public static class Student { } }
上面的程序,我们使用Optional.of(null)方法,这时候程序会第一时间抛出空指针异常,这可以帮助我们尽早发现问题。
我们再看另外一个例子,我们使用Optional.absent方法来初始化posibleNull实例,然后我们get此对象,看看会是什么情况。
public class OptionalDemo { public static void main(String[] args) { Optional<Student> possibleNull = Optional.absent(); Student jim = possibleNull.get(); } public static class Student { } }
运行上面的程序,发现出现了:Exception in thread "main" java.lang.IllegalStateException: Optional.get() cannot be called on an absent value。
这样使用也会有异常出来,那Optional到底有什么意义呢?
使用Optional除了赋予null语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional迫使你积极思考引用缺失的情况,因为你必须显式地从Optional获取引用。直接使用null很容易让人忘掉某些情形,尽管FindBugs可以帮助查找null相关的问题,但是我们还是认为它并不能准确地定位问题根源。
如同输入参数,方法的返回值也可能是null。和其他人一样,你绝对很可能会忘记别人写的方法method(a,b)会返回一个null,就好像当你实现method(a,b)时,也很可能忘记输入参数a可以为null。将方法的返回类型指定为Optional,也可以迫使调用者思考返回的引用缺失的情形。
google guava Throwables帮你抛出异常,处理异常
guava类库中的Throwables提供了一些异常处理的静态方法,这些方法的从功能上分为两类,一类是帮你抛出异常,另外一类是帮你处理异常。
也许你会想:为什么要帮我们处理异常呢?我们自己不会抛出异常吗?
假定下面的方法是我们要调用的方法。
public void doSomething() throws Throwable { //ignore method body } public void doSomethingElse() throws Exception { //ignore method body }
这两个方法的签名一个throws出了Throwable另外一个throws出了Exception,他们没有定义具体会抛出什么异常,也就是说他们什么异常都有可能抛出来,如果我们要调用这样的方法,就需要对他们的异常做一些处理了,我们需要判断什么样的异常需要抛出去,什么样的异常需要封装成RuntimeException。而这些事情就是Throwables类要帮我们做的事情。
假定我们要实现一个doIt的方法,该方法要调用doSomething方法,而doIt的定义中只允许抛出SQLException,我们可以这样做:
public void doIt() throws SQLException { try { doSomething(); } catch (Throwable throwable) { Throwables.propagateIfInstanceOf(throwable, SQLException.class); Throwables.propagateIfPossible(throwable); } }
请注意doIt的catch块,下面这行代码的意思是如果异常的类型是SQLException,那么抛出这个异常
Throwables.propagateIfInstanceOf(throwable, SQLException.class);
第二行表示如果异常是Error类型,那么抛出这个类型,否则将抛出RuntimeException,我们知道RuntimeException是不需要在throws中声明的。
Throwables.propagateIfPossible(throwable);
Throwables类还为我们提供了一些方便的异常处理帮助方法:
- 我们可以通过Throwables.getRooCause(Throwable)获得根异常
- 可以使用getCausalChain方法获得异常的列表
- 可以通过getStackTraceAsString获得异常堆栈的字符串
google guava的BiMap:双向Map
我们知道Map是一种键值对映射,这个映射是键到值的映射,而BiMap首先也是一种Map,他的特别之处在于,既提供键到值的映射,也提供值到键的映射,所以它是双向Map.
想象这么一个场景,我们需要做一个星期几的中英文表示的相互映射,例如Monday对应的中文表示是星期一,同样星期一对应的英文表示是Monday。这是一个绝好的使用BiMap的场景。
package cn.outofmemory.guava.collection;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
public class BiMapDemo {
public static void main(String[] args) {
BiMap<String,String> weekNameMap = HashBiMap.create();
weekNameMap.put("星期一","Monday");
weekNameMap.put("星期二","Tuesday");
weekNameMap.put("星期三","Wednesday");
weekNameMap.put("星期四","Thursday");
weekNameMap.put("星期五","Friday");
weekNameMap.put("星期六","Saturday");
weekNameMap.put("星期日","Sunday");
System.out.println("星期日的英文名是" + weekNameMap.get("星期日"));
System.out.println("Sunday的中文是" + weekNameMap.inverse().get("Sunday"));
}
}
BiMap的值键对的Map可以通过inverse()方法得到。
BiMap的常用实现有:
- HashBiMap: key 集合与 value 集合都有 HashMap 实现
- EnumBiMap: key 与 value 都必须是 enum 类型
- ImmutableBiMap: 不可修改的 BiMap
google guava的Multimaps:一键多值的Map
有时候我们需要这样的数据类型Map<String,Collection<String>>,guava中的Multimap就是为了解决这类问题的。
Multimap的实现
Multimap提供了丰富的实现,所以你可以用它来替代程序里的Map<K, Collection<V>>,具体的实现如下:
实现 | Key实现 | Value实现 |
ArrayListMultimap | HashMap | ArrayList |
HashMultimap | HashMap | HashSet |
LinkedListMultimap | LinkedHashMap | LinkedList |
LinkedHashMultimap | LinkedHashMap | LinkedHashSet |
TreeMultimap | TreeMap | TreeSet |
ImmutableListMultimap | ImmutableMap | ImmutableList |
ImmutableSetMultimap | ImmutableMap | ImmutableSet |
我们通过一个示例来了解Multimap的使用方法:
public class MutliMapTest { public static void main(String... args) { Multimap<String, String> myMultimap = ArrayListMultimap.create(); // 添加键值对 myMultimap.put("Fruits", "Bannana"); //给Fruits元素添加另一个元素 myMultimap.put("Fruits", "Apple"); myMultimap.put("Fruits", "Pear"); myMultimap.put("Vegetables", "Carrot"); // 获得multimap的size int size = myMultimap.size(); System.out.println(size); // 4 // 获得Fruits对应的所有的值 Collection<string> fruits = myMultimap.get("Fruits"); System.out.println(fruits); // [Bannana, Apple, Pear] Collection<string> vegetables = myMultimap.get("Vegetables"); System.out.println(vegetables); // [Carrot] //遍历Mutlimap for(String value : myMultimap.values()) { System.out.println(value); } // Removing a single value myMultimap.remove("Fruits","Pear"); System.out.println(myMultimap.get("Fruits")); // [Bannana, Pear] // Remove all values for a key myMultimap.removeAll("Fruits"); System.out.println(myMultimap.get("Fruits")); // [] (Empty Collection!) } }
google guava集合之Table
在guava库中还提供了一种二维表结构:Table。使用Table可以实现二维矩阵的数据结构,可以是稀溜矩阵。
我们看一个使用示例:
import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import java.util.Map; public class TableDemo { public static void main(String[] args) { Table<Integer, Integer, String> table = HashBasedTable.create(); for (int row = 0; row < 10; row++) { for (int column = 0; column < 5; column++) { table.put(row, column, "value of cell (" + row + "," + column + ")"); } } for (int row=0;row<table.rowMap().size();row++) { Map<Integer,String> rowData = table.row(row); for (int column =0;column < rowData.size(); column ++) { System.out.println("cell(" + row + "," + column + ") value is:" + rowData.get(column)); } } } }
在上面示例中我们通过HashBasedTable创建了一个行类型为Integer,列类型也为Integer,值为String的Table。然后我们使用put方法向Table中添加了一些值,然后显示这些值。
Guava集合:使用Iterators简化Iterator操作
Iterators是Guava中对Iterator迭代器操作的帮助类,这个类提供了很多有用的方法来简化Iterator的操作。
1. 判断迭代器中的元素是否都满足某个条件 all 方法
List<String> list = Lists.newArrayList("Apple","Pear","Peach","Banana"); Predicate<String> condition = new Predicate<String>() { @Override public boolean apply(String input) { return ((String)input).startsWith("P"); } }; boolean allIsStartsWithP = Iterators.all(list.iterator(), condition); System.out.println("all result == " + allIsStartsWithP);
all方法的第一个参数是Iterator,第二个参数是Predicate<String>的实现,这个方法的意义是不需要我们自己去写while循环了,他的内部实现中帮我们做了循环,把循环体中的条件判断抽象出来了。
2. 通过any判断迭代器中是否有一个满足条件的记录,any方法的参数和all方法一样,就不再具体举例了
3. get方法获得迭代器中的第x个元素
String secondElement = Iterators.get(list.iterator(), 1);
4. filter方法过滤符合条件的项
Iterator<String> startPElements = Iterators.filter(list.iterator(), new Predicate<String>() { @Override public boolean apply(String input) { return input.startsWith("P"); } });
filter方法的第一个参数是源迭代器,第二个参数是Predicate的实现,其apply方法会返回当前元素是否符合条件。
5. find方法返回符合条件的第一个元素
String length5Element = Iterators.find(list.iterator(), new Predicate<String>() { @Override public boolean apply(String input) { return input.length() == 5; } });
6. transform方法,对迭代器元素做转换
Iterator<Integer> countIterator = Iterators.transform(list.iterator(), new Function<String, Integer>() { @Override public Integer apply(String input) { return input.length(); } });
上面的例子中我们将字符串转换成了其长度,transform方法输出的是另外一个Iterator.
Guava Files中的文件操作
Java的基本API对文件的操作很繁琐,为了向文件中写入一行文本,都需要写十几行的代码。guava对此作了很多改进,提供了很多方便的操作。
一. Guava的文件写入
Guava的Files类中提供了几个write方法来简化向文件中写入内容的操作,下面的例子演示 Files.write(byte[],File)的用法。
/**
* 演示向文件中写入字节流
*
* @param fileName 要写入文件的文件名
* @param contents 要写入的文件内容
*/
public void demoFileWrite(final String fileName, final String contents)
{
checkNotNull(fileName, "Provided file name for writing must NOT be null.");
checkNotNull(contents, "Unable to write null contents.");
final File newFile = new File(fileName);
try
{
Files.write(contents.getBytes(), newFile);
}
catch (IOException fileIoEx)
{
err.println( "ERROR trying to write to file '" + fileName + "' - "
+ fileIoEx.toString());
}
}
需要注意上面的代码,需要import一些类和方法:
import static com.google.common.base.Preconditions.*;
import static java.lang.System.*;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
另外在演示代码中我捕获了IOException,并打印了一下错误信息。
二.获得文件内容
Files类提供了readLines方法可以方便的读取文件的内容,如下demo代码:
String testFilePath = "d:\\test.txt"; File testFile = new File(testFilePath); List<String> lines = Files.readLines(testFile, Charsets.UTF_16); for (String line : lines) { System.out.println(line); }
注意这里的readLines方法返回的是List<String>的接口,这对于大文件处理是会有问题的。大文件处理可以使用readLines方法的另一个重载。下面的例子演示从一个大文件中逐行读取文本,并做行号计数。
package main.com.jd.coo.guava.io; import java.io.File; import java.io.IOException; import com.google.common.base.Charsets; import com.google.common.io.Files; import com.google.common.io.LineProcessor; /** * Created by outofmemory.cn on 2014/7/24. */ public class FilesDemo { static class CounterLine implements LineProcessor<Integer> { private int rowNum = 0; @Override public boolean processLine(String line) throws IOException { rowNum ++; return true; } @Override public Integer getResult() { return rowNum; } } public static void main(String[] args) throws IOException { String testFilePath = "d:\\test.txt"; File testFile = new File(testFilePath); CounterLine counter = new CounterLine(); Files.readLines(testFile, Charsets.UTF_16, counter); System.out.println(counter.getResult()); } }
这个readLines的重载,需要我们实现一个LineProcessor的泛型接口,在这个接口的实现方法processLine方法中我们可以对行文本进行处理,getResult方法可以获得一个最终的处理结果,这里我们只是简单的返回了一个行计数。
另外还有readBytes方法可以对文件的字节做处理,readFirstLine可以返回第一行的文本,Files.toString(File,Charset)可以返回文件的所有文本内容。
三. 复制移动(剪切)文件
在Guava中复制文件操作提供了一组的copy方法,我们看一个示例:
/** * 演示如何使用guava的Files.copy方法复制文件 * * @param sourceFileName 复制的源文件名 * @param targetFileName 目标文件名 */ public void demoSimpleFileCopy( final String sourceFileName, final String targetFileName) { checkNotNull(sourceFileName, "Copy source file name must NOT be null."); checkNotNull(targetFileName, "Copy target file name must NOT be null."); final File sourceFile = new File(sourceFileName); final File targetFile = new File(targetFileName); try { Files.copy(sourceFile, targetFile); } catch (IOException fileIoEx) { err.println( "ERROR trying to copy file '" + sourceFileName + "' to file '" + targetFileName + "' - " + fileIoEx.toString()); } }
Guava中移动文件使用move方法,用法和copy一样。
四. 比较文件内容
Guava中提供了Files.equal(File,File)方法来比较两个文件的内容是否完全一致,请看下面的示例:
/** * 演示 Files.equal(File,File) 来比较两个文件的内容 * * @param fileName1 比较的文件1文件名 * @param fileName2 比较的文件2文件名 */ public void demoEqual(final String fileName1, final String fileName2) { checkNotNull(fileName1, "First file name for comparison must NOT be null."); checkNotNull(fileName2, "Second file name for comparison must NOT be null."); final File file1 = new File(fileName1); final File file2 = new File(fileName2); try { out.println( "File '" + fileName1 + "' " + (Files.equal(file1, file2) ? "IS" : "is NOT") + " the same as file '" + fileName2 + "'."); } catch (IOException fileIoEx) { err.println( "ERROR trying to compare two files '" + fileName1 + "' and '" + fileName2 + "' - " + fileIoEx.toString()); } }
五. 其他有用的方法
Guava的Files类中还提供了其他一些文件的简捷方法。比如
- touch方法创建或者更新文件的时间戳。
- createTempDir()方法创建临时目录
- Files.createParentDirs(File) 创建父级目录
- getChecksum(File)获得文件的checksum
- hash(File)获得文件的hash
- map系列方法获得文件的内存映射
- getFileExtension(String)获得文件的扩展名
- getNameWithoutExtension(String file)获得不带扩展名的文件名
Guava的方法都提供了一些重载,这些重载可以扩展基本用法,我们也有必要去多了解一下,这些重载的方法。
guava反射TypeToken解决泛型运行时类型擦除的问题
guava反射包中的TypeToken类是用来解决java运行时泛型类型被擦除的问题的,有点不好理解,我们通过一个例子来认识什么是泛型的运行时类型擦除。
ArrayList<String> stringList = Lists.newArrayList(); ArrayList<Integer> intList = Lists.newArrayList(); System.out.println("intList type is " + intList.getClass()); System.out.println("stringList type is " + stringList.getClass()); System.out.println(stringList.getClass().isAssignableFrom(intList.getClass()));
上面的代码我们声明了两个泛型的ArrayList类型,一个泛型的类型参数是String,另外一个是Integer;然后我们输出了两个泛型的Class,并输出两个list的类型是否是同一个list。我们看下输出的结果:
intList type is class java.util.ArrayList
stringList type is class java.util.ArrayList
true
前两个输出都是java.util.ArrayList,而第三个输出竟然是true,也就是认为stringList和intList的类型是一样的。这就是所谓的泛型类型擦除。运行时我们不知道泛型类型的类型参数是什么了。
TypeToken可以解决这个问题,请看下面代码:
TypeToken<ArrayList<String>> typeToken = new TypeToken<ArrayList<String>>() {};
TypeToken<?> genericTypeToken = typeToken.resolveType(ArrayList.class.getTypeParameters()[0]);
System.out.println(genericTypeToken.getType());
注意上面第一行代码使用了一个空的匿名类。第二行使用了resolveType方法解析出泛型类型,第三行代码打印出泛型类型,输出是:
class java.lang.String
可以看出TypeToken解析出了泛型参数的具体类型。
TypeToken的方法列表如下:
方法 | 描述 |
getType() | 获得包装的java.lang.reflect.Type. |
getRawType() | 返回大家熟知的运行时类 |
getSubtype(Class<?>) | 返回那些有特定原始类的子类型。举个例子,如果这有一个Iterable并且参数是List.class,那么返回将是List。 |
getSupertype(Class<?>) | 产生这个类型的超类,这个超类是指定的原始类型。举个例子,如果这是一个Set并且参数是Iterable.class,结果将会是Iterable。 |
isAssignableFrom(type) | 如果这个类型是 assignable from 指定的类型,并且考虑泛型参数,返回true。List<? extends Number>是assignable from List,但List没有. |
getTypes() | 返回一个Set,包含了这个所有接口,子类和类是这个类型的类。返回的Set同样提供了classes()和interfaces()方法允许你只浏览超类和接口类。 |
isArray() | 检查某个类型是不是数组,甚至是<? extends A[]>。 |
getComponentType() | 返回组件类型数组。 |
guava反射:Reflection.newProxy方法简化动态代理
原理上Google Guava的动态代理也是使用JDK的动态代理,这是做了封装,更加简便。另外一点是能够很好的检查需要代理的对象必须拥有接口。使用Class类的isInterface()
来做检查。
下面我们先比较一下jdk动态代理和guava动态代理的实现:
JDK动态代理:
Foo foo = (Foo) Proxy.newProxyInstance(
Foo.class.getClassLoader(),
new Class<?>[] {Foo.class},
invocationHandler);
Guava动态代理:
Foo foo = Reflection.newProxy(Foo.class, invocationHandler);
可以看出使用Guava的方式更简洁一些,下面我们用一个具体的例子来看下:
package cn.outofmemory.guava.reflect; import com.google.common.reflect.Reflection; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * Created by outofmemory.cn on 2014/7/31. */ public class DynamicProxyDemo { public static void main(String[] args) { InvocationHandler invocationHandler = new MyInvocationHandler(); // Guava Dynamic Proxy implement IFoo foo = Reflection.newProxy(IFoo.class, invocationHandler); foo.doSomething(); //jdk Dynamic proxy implement IFoo jdkFoo = (IFoo) Proxy.newProxyInstance( IFoo.class.getClassLoader(), new Class<?>[]{IFoo.class}, invocationHandler); jdkFoo.doSomething(); } public static class MyInvocationHandler implements InvocationHandler{ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy println something"); return null; } } public static interface IFoo { void doSomething(); } }
就是这样了,非常简单。
Guava并发:ListenableFuture使用介绍以及示例
ListenableFuture顾名思义就是可以监听的Future,它是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。如果我们希望一旦计算完成就拿到结果展示给用户或者做另外的计算,就必须使用另一个线程不断的查询计算状态。这样做,代码复杂,而且效率低下。使用ListenableFuture Guava帮我们检测Future是否完成了,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。
ListenableFuture是一个接口,它从jdk的Future接口继承,添加了void addListener(Runnable listener, Executor executor)
方法。
我们看下如何使用ListenableFuture。首先需要定义ListenableFuture的实例。
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); final ListenableFuture<Integer> listenableFuture = executorService.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("call execute.."); TimeUnit.SECONDS.sleep(1); return 7; } });
首先通过MoreExecutors类的静态方法listeningDecorator方法初始化一个ListeningExecutorService的方法,然后使用此实例的submit方法即可初始化ListenableFuture对象。
我们上文中定义的ListenableFuture要做的工作,在Callable接口的实现类中定义,这里只是休眠了1秒钟然后返回一个数字7.
有了ListenableFuture实例,有两种方法可以执行此Future并执行Future完成之后的回调函数。
方法一:通过ListenableFuture的addListener方法
listenableFuture.addListener(new Runnable() { @Override public void run() { try { System.out.println("get listenable future's result " + listenableFuture.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }, executorService);
方法二:通过Futures的静态方法addCallback给ListenableFuture添加回调函数
Futures.addCallback(listenableFuture, new FutureCallback<Integer>() { @Override public void onSuccess(Integer result) { System.out.println("get listenable future's result with callback " + result); } @Override public void onFailure(Throwable t) { t.printStackTrace(); } });
推荐使用第二种方法,因为第二种方法可以直接得到Future的返回值,或者处理错误情况。本质上第二种方法是通过调动第一种方法实现的,做了进一步的封装。
另外ListenableFuture还有其他几种内置实现:
- SettableFuture:不需要实现一个方法来计算返回值,而只需要返回一个固定值来做为返回值,可以通过程序设置此Future的返回值或者异常信息
- CheckedFuture: 这是一个继承自ListenableFuture接口,他提供了checkedGet()方法,此方法在Future执行发生异常时,可以抛出指定类型的异常。
Guava并发:RateLimiter限制资源的并发访问线程数
RateLimiter类似于JDK的信号量Semphore,他用来限制对资源并发访问的线程数。
RateLimiter limiter = RateLimiter.create(4.0); //每秒不超过4个任务被提交 limiter.acquire(); //请求RateLimiter, 超过permits会被阻塞 executor.submit(runnable); //提交任务
也可以以非阻塞的形式来使用:
If(limiter.tryAcquire()){ //未请求到limiter则立即返回false doSomething(); }else{ doSomethingElse(); }
Guava并发:使用Monitor控制并发
Monitor就像java原生的synchronized, ReentrantLock一样,每次只允许一个线程执行代码块,且可重占用,每一次占用要对应一次退出占用。
/** * 通过Monitor的Guard进行条件阻塞 */ public class MonitorSample { private List<String> list = new ArrayList<String>(); private static final int MAX_SIZE = 10; private Monitor monitor = new Monitor(); private Monitor.Guard listBelowCapacity = new Monitor.Guard(monitor) { @Override public boolean isSatisfied() { return list.size() < MAX_SIZE; } }; public void addToList(String item) throws InterruptedException { monitor.enterWhen(listBelowCapacity); //Guard(形如Condition),不满足则阻塞,而且我们并没有在Guard进行任何通知操作 try { list.add(item); } finally { monitor.leave(); } } }
就如上面,我们通过if条件来判断是否可进入Monitor代码块,并再try/finally中释放:
if (monitor.enterIf(guardCondition)) { try { doWork(); } finally { monitor.leave(); } }
其他的Monitor访问方法:
- Monitor.enter //进入Monitor块,将阻塞其他线程直到Monitor.leave
- Monitor.tryEnter //尝试进入Monitor块,true表示可以进入, false表示不能,并且不会一直阻塞
- Monitor.tryEnterIf //根据条件尝试进入Monitor块
这几个方法都有对应的超时设置版本。