一、概述
Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。
guava的优点:
高效设计良好的API,被Google的开发者设计,实现和使用
遵循高效的java语法实践
使代码更刻度,简洁,简单
节约时间,资源,提高生产力
Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:
集合 [collections]
缓存 [caching]
原生类型支持 [primitives support]
并发库 [concurrency libraries]
通用注解 [common annotations]
字符串处理 [string processing]
I/O 等等。
二、基本使用
pom依赖
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
三、Guava的具体应用
1 String的相关操作
1.1 字符串拼接
字符串的拼接采用的是Guava中的Joiner类中的方法
@Test
public void testJdkJoin() {
List<String> strList = Lists.newArrayList("1", "2", null, "3", "4");
//字符串拼接,无法跳过空字符串
String str = String.join(",", strList);
Assert.assertEquals("1,2,3,4", str);
}
@Test
public void testGuavaJoin() {
List<String> strList = Lists.newArrayList("1", "2", null, "3", "4");
//字符串拼接,跳过空字符串
String str = Joiner.on(",").skipNulls().join(strList);
Assert.assertEquals("1,2,3,4", str);
}
1.2字符串分割
@Test
public void testJdkSplit(){
String str = "1,2, 3 ,,4,";
//trimResults():去除空格,omitEmptyStrings():删除空数组
String[] strList = str.split(",");
// List<String> strList = Splitter.on(",").trimResults().omitEmptyStrings().splitToList(str);
for (int i = 0; i < strList.length; i++) {
String s = strList[i];
System.out.println(s);
}
Assert.assertEquals(4,strList.length);
}
@Test
public void testGuavaSplit(){
String str = "1,2, 3 ,,4,";
//trimResults():去除空格,omitEmptyStrings():删除空数组
List<String> strList = Splitter.on(",").trimResults().omitEmptyStrings().splitToList(str);
strList.forEach(s -> System.out.println(s));
Assert.assertEquals(4,strList.size());
}
2 不可变集合
2.1为什么使用不可变集合
不可变对象有很多优点,包括:
当对象被不可信的库调用时,不可变形式是安全的;
不可变对象被多个线程调用时,不存在竞态条件问题
不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
不可变对象因为有固定不变,可以作为常量来安全使用。
所有Guava不可变集合的实现都不接受null值。我们对Google内部的代码库做过详细研究,发现只有5%的情况需要在集合中允许null元素,剩下的95%场景都是遇到null值就快速失败。如果你需要在不可变集合中使用null,请使用JDK中的Collections.unmodifiableXXX方法。更多细节建议请参考“使用和避免null”。
2.2、JDK中的Collections.unmodifiableXXX方法
// jdk
@Test
public void testJDKImmutable(){
List<String> list=new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
//通过list创建一个不可变的unmodifiableList集合
List<String> unmodifiableList= Collections.unmodifiableList(list);
System.out.println(unmodifiableList);
//通过list添加元素
list.add("ddd");
System.out.println("往list添加一个元素:"+list);
System.out.println("通过list添加元素之后的unmodifiableList:"+unmodifiableList);
//通过unmodifiableList添加元素
unmodifiableList.add("eee");
System.out.println("往unmodifiableList添加一个元素:"+unmodifiableList);
}
输出
[a, b, c]
往list添加一个元素:[a, b, c, ddd]
通过list添加元素之后的unmodifiableList:[a, b, c, ddd]
java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.add(Collections.java:1055)
通过运行结果我们可以看出:虽然unmodifiableList不可以直接添加元素,但是我的list是可以添加元素的,而list的改变也会使unmodifiableList改变。
所以说Collections.unmodifiableList实现的不是真正的不可变集合。
2.3 Guava 的immutable集合
Guava提供了对JDK里标准集合类里的immutable版本的简单方便的实现,以及Guava自己的一些专门集合类的immutable实现。当你不希望修改一个集合类,
或者想做一个常量集合类的时候,使用immutable集合类就是一个最佳的编程实践。
注意:每个Guava immutable集合类的实现都拒绝null值。我们做过对Google内部代码的全面的调查,并且发现只有5%的情况下集合类允许null值,而95%的情况下
都拒绝null值。万一你真的需要能接受null值的集合类,你可以考虑用Collections.unmodifiableXXX。
immutable集合可以有以下几种方式来创建:
1、用copyOf方法, 譬如, ImmutableSet.copyOf(set)
2、使用of方法,譬如,ImmutableSet.of(“a”, “b”, “c”)或者ImmutableMap.of(“a”, 1, “b”, 2)
3、使用Builder类
@Test
public void testGuavaImmutable(){
List<String> list=new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
//ImmutableList.copyOf
ImmutableList<String> imlist=ImmutableList.copyOf(list);
System.out.println("imlist:"+imlist);
//ImmutableList.of
ImmutableList<String> imOflist=ImmutableList.of("peida","jerry","harry");
System.out.println("imOflist:"+imOflist);
ImmutableSortedSet<String> imSortList=ImmutableSortedSet.of("a", "b", "c", "a", "d", "b");
System.out.println("imSortList:"+imSortList);
list.add("baby");
//关键看这里是否imlist也添加新元素了
System.out.println("list添加新元素之后看imlist:"+imlist);
ImmutableSet<Color> imColorSet =
ImmutableSet.<Color>builder()
.add(new Color(0, 255, 255))
.add(new Color(0, 191, 255))
.build();
System.out.println("imColorSet:"+imColorSet);
}
智能的copyOf
copyOf方法比你想象的要智能,ImmutableXXX.copyOf会在合适的情况下避免拷贝元素的操作-先忽略具体的细节,但是它的实现一般都是很“智能”的。譬如:
@Test
public void testCotyOf(){
ImmutableSet<String> imSet=ImmutableSet.of("peida","jerry","harry","lisa");
System.out.println("imSet:"+imSet);
//set直接转list
ImmutableList<String> imlist=ImmutableList.copyOf(imSet);
System.out.println("imlist:"+imlist);
//list直接转SortedSet
ImmutableSortedSet<String> imSortSet=ImmutableSortedSet.copyOf(imSet);
System.out.println("imSortSet:"+imSortSet);
List<String> list=new ArrayList<String>();
for(int i=0;i<=10;i++){
list.add(i+"x");
}
System.out.println("list:"+list);
//截取集合部分元素
ImmutableList<String> imInfolist=ImmutableList.copyOf(list.subList(2, 8));
System.out.println("imInfolist:"+imInfolist);
}
3 集合
Guava引入了很多JDK没有的、但明显有用的新集合类型。这些新类型是为了和JDK集合框架共存,而没有往JDK集合抽象中硬塞其他概念。作为一般规则,Guava集合非常精准地遵循了JDK接口契约。
3.1、MultiSet[无序+可重复]-工具类Multisets
Guava提供了一个新集合类型 Multiset,它可以多次添加相等的元素。维基百科从数学角度这样定义Multiset:”集合[set]概念的延伸,它的元素可以重复出现…与集合[set]相同而与元组[tuple]相反的是,Multiset元素的顺序是无关紧要的:Multiset {a, a, b}和{a, b, a}是相等的”。——译者注:这里所说的集合[set]是数学上的概念,Multiset继承自JDK中的Collection接口,而不是Set接口,所以包含重复元素并没有违反原有的接口契约。
这个接口没有实现java.util.Set接口,Set接口规定里面是不能够放入重复的元素,如果放入重复元素会被覆盖掉的;然而Multiset接口却是可以放入重复元素的,Set接口中的元素是[1,2,3],Multiset中确可以[1✖️2,2✖️3,3✖️3]来表示多个相同的元素。
@Test
public void testMultiSet(){
Multiset<String> multiset= HashMultiset.create();
multiset.add("aa");
multiset.add("bb");
multiset.add("cc",2);
System.out.println(multiset);//[aa, bb, cc x 2]
System.out.println(multiset.size()); //4
System.out.println(multiset.count("cc"));//2
multiset.setCount("bb",4);
System.out.println(multiset);//[aa, bb x 4, cc x 2]
}
3.2、SortedMultiset
SortedMultiset是Multiset 接口的变种,它支持高效地获取指定范围的子集。比方说,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()来统计你的站点中延迟在100毫秒以内的访问,然后把这个值和latencies.size()相比,以获取这个延迟水平在总体访问中的比例。
3.3、MultiMap[key-value key可以重复 ]-工具类Multimaps
程序开发中使用Map<K, List>或Map<K, Set>,并且要忍受这个结构的笨拙。例如,Map<K, Set>通常用来表示非标定有向图。Guava的 Multimap可以很容易地把一个键映射到多个值。换句话说,Multimap是把键映射到任意多个值的一般方式。
很少会直接使用Multimap接口,更多时候你会用ListMultimap或SetMultimap接口,它们分别把键映射到List或Set。
Multimap提供了多种形式的实现。在大多数要使用Map<K, Collection>的地方,你都可以使用它们
除了两个不可变形式的实现,其他所有实现都支持null键和null值
*LinkedListMultimap.entries()保留了所有键和值的迭代顺序。详情见doc链接。
**LinkedHashMultimap保留了映射项的插入顺序,包括键插入的顺序,以及键映射的所有值的插入顺序。
请注意,并非所有的Multimap都和上面列出的一样,使用Map<K, Collection>来实现(特别是,一些Multimap实现用了自定义的hashTable,以最小化开销)
如果你想要更大的定制化,请用Multimaps.newMultimap(Map, Supplier)或list和 set版本,使用自定义的Collection、List或Set实现Multimap。
@Test
public void testMultiMap() {
Multimap<String, String> multimap = ArrayListMultimap.create();
multimap.put("fruit", "bannana");
multimap.put("fruit", "apple");//key可以重复
multimap.put("fruit", "apple");//value可以重复,不会覆盖之前的
multimap.put("fruit", "peach");
multimap.put("fish", "crucian");//欧洲鲫鱼
multimap.put("fish", "carp");//鲤鱼
Collection<String> fruits = multimap.get("fruit");
System.err.println(fruits);//[bannana, apple, apple, peach]
//对比 HashMultimap
Multimap<String,String> multimap2= HashMultimap.create();
multimap2.put("fruit2", "bannana");
multimap2.put("fruit2", "apple");
multimap2.put("fruit2", "apple");
System.err.println(multimap2.size());//2
System.err.println(multimap2.get("fruit2"));//[apple, bannana] 注意: 这里只有一个apple
}
3.4、BiMap[双向Map(Bidirectional Map) 键与值都不能重复]
传统上,实现键值对的双向映射需要维护两个单独的map,并保持它们间的同步。但这种方式很容易出错,而且对于值已经在map中的情况,会变得非常混乱。例如:
Map<String, Integer> nameToId = Maps.newHashMap();
Map<Integer, String> idToName = Maps.newHashMap();
nameToId.put("Bob", 42);
idToName.put(42, "Bob");
//如果"Bob"和42已经在map中了,会发生什么?
//如果我们忘了同步两个map,会有诡异的bug发生...
BiMap<K, V>是特殊的Map:
可以用 inverse()反转BiMap<K, V>的键值映射
保证值是唯一的,因此 values()返回Set而不是普通的Collection
在BiMap中,如果你想把键映射到已经存在的值,会抛出IllegalArgumentException异常。如果对特定值,你想要强制替换它的键,请使用 BiMap.forcePut(key, value)。
@Test
public void testBiMap() {
BiMap<String, Integer> userId = HashBiMap.create();
userId.put("lhx",30);
userId.put("zll",28);
String userForId = userId.inverse().get(30);
System.out.println(userForId);//lhx
userId.put("jm",30);//报错
String userForId2 = userId.inverse().get(30);
System.out.println(userForId2);//lhx
}
3.5、Table【双键的Map Map–> Table–>rowKey+columnKey+value //和sql中的联合主键有点像】-工具类Tables
行、列、值。当使用多个键做索引的时候,可能会用类似Map<FirstName, Map<LastName, Person>>的实现,这种方式很丑陋,使用上也不友好。
Guava为此提供了新集合类型Table,它有两个支持所有类型的键:”行”和”列”。
Table有如下几种实现:
HashBasedTable:本质上用HashMap<R, HashMap<C, V>>实现;
TreeBasedTable:本质上用TreeMap<R, TreeMap<C,V>>实现;
ImmutableTable:本质上用ImmutableMap<R, ImmutableMap<C, V>>实现;注:ImmutableTable对稀疏或密集的数据集都有优化。
ArrayTable:要求在构造时就指定行和列的大小,本质上由一个二维数组实现,以提升访问速度和密集Table的内存利用率。ArrayTable与其他Table的工作原理有点不同,请参见Javadoc了解详情。
@Test
public void testTable() {
Table<String, String, Integer> table = HashBasedTable.create();
table.put("a", "b", 4);
table.put("a", "c", 20);
table.put("b", "c", 5);
Map<String, Integer> a = table.row("a");// returns a Map mapping {b=4, c=20}
System.out.println(a);
Map<String, Integer> column = table.column("c");// returns a Map mapping {a=20, b=5}
System.out.println(column);
Integer integer = table.get("a", "c");
System.out.println(integer); //20
}
4 Guava 缓存
Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。
通常来说,Guava Cache适用于:
你愿意消耗一些内存空间来提升速度。
你预料到某些键会被查询一次以上。
缓存中存放的数据总量不会超出内存容量。(Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试redis这类工具)
如果你的场景符合上述的每一条,Guava Cache就适合你。
注:如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的内存效率——但Cache的大多数特性都很难基于旧有的ConcurrentMap复制,甚至根本不可能做到。
4.1、Guava Cache有以下两种创建方式:
通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于,这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值。但不同的在于cacheloader的定义比较宽泛,是针对整个cache定义的,可以认为是统一的根据key值load value的方法。而callable的方式较为灵活,允许你在get的时候指定。
方式一、创建 CacheLoader
LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。例如,你可以用下面的代码构建LoadingCache:
CacheLoader: 当检索不存在的时候,会自动的加载信息的
class Person{
private String name;
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
public com.google.common.cache.CacheLoader<String, Person> createCacheLoader() {
return new com.google.common.cache.CacheLoader<String, Person>() {
@Override
public Person load(String key) throws Exception {
System.out.println("加载创建key:" + key);
return new Person(key+":ddd");
}
};
}
@Test
public void testCreateCacheLoader() throws ExecutionException {
LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(createCacheLoader());
cache.put("aa",new Person("aaa"));
Person aa = cache.get("aa");
System.out.println(aa);//Person{name='aaa'}
Person bb = cache.get("bb");
System.out.println(bb); //加载创建key:bb Person{name='bb:ddd'}
}
方式二、创建 Callable
@Test
public void testCreateCallable() throws Exception {
Cache<String, Person> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
try {
cache.put("aa", new Person("aaaa"));
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
Person aa = cache.get("aa", new Callable<Person>() {
@Override
public Person call() throws Exception {
return new Person("defalut");
// return doThingsTheHardWay(key);
}
});
System.out.println(aa);//Person{name='aaaa'}
} catch (Exception e) {
throw new Exception(e.getCause());
}
Person bb = cache.get("bb", () -> new Person("defalut"));
System.out.println(bb); //Person{name='defalut'}
}
4.2、显示插入数据
使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中
进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable) 应该总是优先使用。
4.3、缓存回收
Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。
1、基于容量的回收(size-based eviction)
大小
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。
警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。
权重
另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。
@Test
public void testWeight() throws Exception {
LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
.maximumWeight(5)
.weigher((Weigher<String, Person>) (s, person) -> {
//权重计算器
int weight = person.name.length();
System.out.println("key:"+s);
return weight;
})
.build(new CacheLoader<String, Person>() {
@Override
public Person load(String key) {
System.out.println("加载创建key:" + key);
return new Person(key + ":default");
}
});
cache.put("a",new Person("aaaaaaa1"));
cache.put("b",new Person("bbbbbb1"));
cache.put("c",new Person("cc1"));
Person a = cache.get("a");
System.out.println(a);
Person b = cache.get("b");
System.out.println(b);
Person c = cache.get("c");
System.out.println(c);
//缓存只有 一个 c
System.out.println(cache.asMap());
}
2、定时回收(Timed Eviction)
acheBuilder提供两种定时回收的方法:
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。【读一次多久后没有被访问过期】
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。【写完多久后过期】
如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。
@Test
public void testEvictionByAccessTime() throws ExecutionException, InterruptedException {
LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS)
.build(createCacheLoader());
cache.getUnchecked("wangji");
TimeUnit.SECONDS.sleep(3);
Person employee = cache.getIfPresent("wangji"); //不会重新加载创建cache
System.out.println("被销毁:" + (employee == null ? "是的" : "否"));
cache.getUnchecked("guava");
TimeUnit.SECONDS.sleep(1);
employee = cache.getIfPresent("guava"); //会重新加载创建cache
System.out.println("被销毁:" + (employee == null ? "是的" : "否"));
TimeUnit.SECONDS.sleep(2);
employee = cache.getIfPresent("guava"); //不会重新加载创建cache
System.out.println("被销毁:" + (employee == null ? "是的" : "否"));
TimeUnit.SECONDS.sleep(2);
employee = cache.getIfPresent("guava"); //不会重新加载创建cache
System.out.println("被销毁:" + (employee == null ? "是的" : "否"));
TimeUnit.SECONDS.sleep(2);
employee = cache.getIfPresent("guava"); //不会重新加载创建cache
System.out.println("被销毁:" + (employee == null ? "是的" : "否"));
}
输出
加载创建key:wangji
被销毁:是的
加载创建key:guava
被销毁:否
被销毁:是的
被销毁:是的
被销毁:是的
3、基于引用的回收(Reference-based Eviction)【强(strong)、软(soft)、弱(weak)、虚(phantom】
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(),使用弱引用键的缓存用而不是equals比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(),使用弱引用值的缓存用而不是equals比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值
4、显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()
清理什么时候发生
使用CacheBuilder构建的缓存不会”自动”执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。
ScheduledExecutorService可以帮助你很好地实现这样的定时调度。
刷新
刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
5、移除监听器
通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。