Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。
Guava 是Java的工具集,提供了一些常用的便利的操作工具类,减少因为 空指针、异步操作等引起的问题BUG,提高开发效率。
本文主要介绍了Guava常用的工具方法,快速入门Guava。
1、基本工具(Base utils)#
1. Optional#
null 值出现在代码中,有如下缺点:
- 语义模糊,引起歧义。例如,Map.get(key)返回Null时,可能表示map中的值是null,亦或map中没有key对应的值。
- 在应用层面可能造成混乱,出现令人意外的错误。
为了尽量避免程序中的null值,guava提供了Optional对数据进行封装。如果值为空则立即抛出异常,并且提供了Absent和Present两个子类分别表示值缺失和值存在的情形,来增强null的语义。
常用方法如下:
-
isPresent():如果Optional包含非null的引用(引用存在),返回true
-
get() :如果Optional为NULL将触发异常
public static void test(){ Optional<Integer> possible = Optional.fromNullable(5); //创建允许null值的Optional if(possible.isPresent()){//包含的引用非null的(引用存在),返回true System.out.println(possible.get()); }else{ System.out.println("possible is null"); } }
-
or(defaultvalue) :包含的引用缺失(null),返回默认的值,否则返回本身
-
orNull():包含的引用缺失,返回null
-
asSet():如果引用存在,返回只有单一元素的集合;若为NULl返回空集合
2. 先决条件 Preconditions#
Preconditions 提供了判断条件是否合法的静态方法,如果不符合要求会抛出异常。类似断言。
方法声明(不包括额外参数) | 描述 | 检查失败时抛出的异常 |
---|---|---|
checkArgument(boolean) | 检查boolean是否为true,用来检查传递给方法的参数 | IllegalArgumentException |
checkNotNull(T) | 检查value是否为null,该方法直接返回value,因此可以内嵌使用checkNotNull | NullPointerException |
checkState(boolean) | 用来检查对象的某些状态。 | IllegalStateException |
checkElementIndex(int index, int size) | 检查index作为索引值对某个列表、字符串或数组是否有效。index>=0 && index<size | IndexOutOfBoundsException |
checkPositionIndex(int index, int size) | 检查index作为位置值对某个列表、字符串或数组是否有效。index>=0 && index<=size | IndexOutOfBoundsException |
checkPositionIndexes(int start, int end, int size) | 检查[start, end]表示的位置范围对某个列表、字符串或数组是否有效 | IndexOutOfBoundsException |
每个判断方法都有三个多态方法:
-
没有额外参数:抛出的异常中没有错误消息;
-
有一个Object对象作为额外参数:抛出的异常使用Object.toString() 作为错误消息;
-
有一个String对象作为额外参数,并且有一组任意数量的附加Object对象:这个变种处理异常消息的方式有点类似printf,但考虑GWT的兼容性和效率,只支持%s指示符。例如:
checkArgument(i >= 0); checkArgument(i >= 0, "Argument was expected nonnegative"); checkArgument(i < j, "Expected i < j, but %s > %s", i, j);
3. 连接器 Joiner#
用分隔符将多个字符串(或数组元素)连接成一个字符串。
常用方法如下:
- on(String):静态工厂方法,生成一个新的 Joiner 对象,参数为连接符
- skipNulls():如果元素为空,则跳过
- useForNull(String):如果元素为空,则用这个字符串代替
- join(数组/链表):要连接的数组/链表
- appendTo(String,数组/链表):在第一个参数后面新加上 拼接后的字符串
- withKeyValueSeparator(String):得到 MapJoiner,连接Map的键、值
@Test
public void test(){
List<String> list1 = Arrays.asList("aa", "bb", "cc");
System.out.println(Joiner.on("-").join(list1));
List<String> list2 = Arrays.asList("aa", "bb", "cc", null, "dd");
System.out.println(Joiner.on("-").skipNulls().join(list2));
System.out.println(Joiner.on("-").useForNull("nulla").join(list2));
Map map = ImmutableMap.of("k1", "v1", "k2", "v2");
System.out.println(Joiner.on("-").withKeyValueSeparator("=").join(map));
}
输出:
aa-bb-cc
aa-bb-cc-dd
aa-bb-cc-null-dd
k1=v1-k2=v2
注意:joiner实例总是不可变的。用来定义joiner目标语义的配置方法总会返回一个新的joiner实例。这使得joiner实例都是线程安全的,你可以将其定义为static final常量。
4. 拆分器 Splitter#
Splitter 能将一个字符串按照分隔符生成字符串集合,是 Joiner的反向操作。
常用方法如下:
-
on(String):静态工厂方法,生成一个新的 Splitter 对象,参数为连接符
-
trimResults():结果去除子串中的空格
-
omitEmptyStrings():去除null的子串
-
split(String):拆分字符串
-
withKeyValueSeparator(String):得到 MapSplitter,拆分成Map的键、值。注意,这个对被拆分字符串格式有严格要求,否则会抛出异常
@Test public void test1(){ String string = " ,a,b,"; System.out.println(Splitter.on(",").split(string)); System.out.println(Splitter.on(",").trimResults().split(string)); System.out.println(Splitter.on(",").omitEmptyStrings().split(string)); System.out.println(Splitter.on(",").trimResults().omitEmptyStrings().split(string)); // 根据长度拆分 string = "12345678"; System.out.println(Splitter.fixedLength(2).split(string)); // 拆分成Map System.out.println(Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4")); }
输出如下:
[ , a, b, ] [, a, b, ] [ , a, b] [a, b] [12, 34, 56, 78] {1=2, 3=4}
5. 字符串处理 Strings#
Strings 类主要提供了对字符串的一些操作。主要方法如下:
-
nullToEmpty(String string) :null字符串转空字符串
-
emptyToNull(String string):空字符串转null字符串
-
isNullOrEmpty(String string):判断字符串为null或空字符串
-
padStart(String string, int minLength, char padChar):如果string的长度小于minLeng,在string前添加padChar,直到字符串长度为minLeng。
@Test public void test(){ String aa = "12345"; // A12345 System.out.println(Strings.padStart(aa, 6, 'A')); // 12345 System.out.println(Strings.padStart(aa, 5, 'A')); }
-
String padEnd(String string, int minLength, char padChar):和padStart类似,不过是在尾部添加新字符串
-
commonPrefix(CharSequence a, CharSequence b):返回共同的前缀
-
commonSuffix(CharSequence a, CharSequence b):返回共同的后缀
@Test
public void test2(){
String aa = "abc123def";
String bb = "abc789def";
System.out.println(Strings.commonPrefix(aa, bb));
System.out.println(Strings.commonSuffix(aa, bb));
}
输出如下:
abc
def
2、集合工具(Collections)#
1. 不可变集合#
不可变集合,即创建后就只可读,不可修改的集合。为什么要使用不可变集合呢?主要有如下优点(摘自官方文档):
- 当对象被不可信的库调用时,不可变形式是安全的;
- 不可变对象被多个线程调用时,不存在竞态条件问题
- 不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
- 不可变对象因为有固定不变,可以作为常量来安全使用。
JDK也提供了Collections.unmodifiableXXX方法把集合包装为不可变形式,但我们认为不够好:
-
笨重而且累赘:不能舒适地用在所有想做防御性拷贝的场景;
-
不安全:要保证没人通过原集合的引用进行修改,返回的集合才是事实上不可变的;
@Test public void test3(){ List<Integer> list = Lists.newArrayList(1,2,3); List<Integer> list1 = Collections.unmodifiableList(list); // [1, 2, 3] System.out.println(list); // [1, 2, 3] System.out.println(list1); // list修改,list1也会被修改 list.add(4); // [1, 2, 3, 4] System.out.println(list1); }
-
低效:包装过的集合仍然保有可变集合的开销,比如并发修改的检查、散列表的额外空间,等等。
注意:所有Guava不可变集合的实现都不接受null值。因为谷歌内部调查代码发现,只有5%的情况需要在集合中允许null元素。如果要存储null值,请使用JDK的Collections.unmodifiable方法
创建不可变集合的几个方法:
-
copyOf 方法,如ImmutableSet.copyOf(set);
-
of方法,如ImmutableSet.of(“a”, “b”, “c”)或 ImmutableMap.of(“a”, 1, “b”, 2);
-
Builder工具,如
public static final ImmutableSet<Color> GOOGLE_COLORS = ImmutableSet.<Color>builder() .addAll(WEBSAFE_COLORS) .add(new Color(0, 191, 255)) .build();
copyOf 是很智能和高效的,在特定会避免线性拷贝。下期有机会来分析下它的实现原理。
关联可变集合和不可变集合
可变集合接口 | 属于JDK还是Guava | 不可变版本 |
---|---|---|
Collection | JDK | ImmutableCollection |
List | JDK | ImmutableList |
Set | JDK | ImmutableSet |
SortedSet/NavigableSet | JDK | ImmutableSortedSet |
Map | JDK | ImmutableMap |
SortedMap | JDK | ImmutableSortedMap |
Multiset | Guava | ImmutableMultiset |
SortedMultiset | Guava | ImmutableSortedMultiset |
Multimap | Guava | ImmutableMultimap |
ListMultimap | Guava | ImmutableListMultimap |
SetMultimap | Guava | ImmutableSetMultimap |
BiMap | Guava | ImmutableBiMap |
ClassToInstanceMap | Guava | ImmutableClassToInstanceMap |
Table | Guava | ImmutableTable |
2. Multiset#
定义摘自维基百科:
”集合[set]概念的延伸,它的元素可以重复出现…与集合[set]相同而与元组[tuple]相反的是,Multiset元素的顺序是无关紧要的:Multiset {a, a, b}和{a, b, a}是相等的”
Multiset继承自JDK中的Collection接口,而不是Set接口,所以可以包含重复元素。可以从以下角度理解:
- 没有元素顺序限制的ArrayList
- Map<E, Integer>,键为元素,值为计数
Multiset提供像无序的ArrayList的基本操作:
- add(E)添加单个给定元素
- iterator()返回一个迭代器,包含Multiset的所有元素(包括重复的元素)
- size()返回所有元素的总个数(包括重复的元素)
当把Multiset看作Map<E, Integer>时,它也提供了Map的查询操作:
- count(Object)返回给定元素的计数。
- entrySet()返回Set<Multiset.Entry>,和Map的entrySet类似。
- elementSet()返回所有不重复元素的Set,和Map的keySet()类似。
常用方法如下:
方法 | 描述 |
---|---|
count(E) | 给定元素在Multiset中的计数 |
elementSet() | Multiset中不重复元素的集合,类型为Set |
entrySet() | 和Map的entrySet类似,返回Set<Multiset.Entry>, 其中包含的Entry支持getElement()和getCount()方法 |
add(E, int) | 增加给定元素在Multiset中的计数 |
remove(E, int) | 减少给定元素在Multiset中的计数 |
setCount(E, int) | 设置给定元素在Multiset中的计数,不可以为负数 |
size() | 返回集合元素的总个数(包括重复的元素) |
应用:统计一个词在文档中出现了多少次。
-
传统的做法是这样的
Map<String, Integer> counts = new HashMap<String, Integer>(); for (String word : words) { Integer count = counts.get(word); if (count == null) { counts.put(word, 1); } else { counts.put(word, count + 1); } }
-
使用Multiset操作:
Multiset<String> multiset = HashMultiset.create(); for (String word : words) { multiset.add(word); } int count = multiset.count("today");
3. Multimap#
通俗来讲,Multimap 是一键对多值的HashMap,类似于 Map<K, List> 的数据结构。
@Test
public void test2() {
Multimap<String, String> multimap = ArrayListMultimap.create();
multimap.put("name", "Jack");
multimap.put("name", "Jack");
multimap.put("name", "Tom");
multimap.put("age", "10");
multimap.put("age", "12");
System.out.println(multimap);
System.out.println(multimap.get("name").size());
}
输出:
{name=[Jack, Jack, Tom], age=[10, 12]}
3
常用操作如下:
方法签名 | 描述 | 等价于 |
---|---|---|
put(K, V) | 添加键到单个值的映射 | multimap.get(key).add(value) |
putAll(K, Iterable) | 依次添加键到多个值的映射 | Iterables.addAll(multimap.get(key), values) |
remove(K, V) | 移除键到值的映射;如果有这样的键值并成功移除,返回true。 | multimap.get(key).remove(value) |
removeAll(K) | 清除键对应的所有值,返回的集合包含所有之前映射到K的值,但修改这个集合就不会影响Multimap了。 | multimap.get(key).clear() |
replaceValues(K, Iterable) | 清除键对应的所有值,并重新把key关联到Iterable中的每个元素。返回的集合包含所有之前映射到K的值。 | multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values) |
主要操作:
- asMap:为Multimap<K, V>提供Map<K,Collection>形式的视图
- entries:返回所有”键-单个值映射”,包括重复键。Collection<Map.Entry<K, V>>类型
- keySet:返回所有不同的键,Set类型
- keys:用Multiset表示Multimap中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个Multiset中移除元素,但不能做添加操作;移除操作会反映到底层的Multimap
- values:用一个”扁平”的Collection包含Multimap中的所有值,包括重复键
4. BiMap#
一般的Map只提供”键-值“的映射,而BiMap则同时提供了”键-值“和”值-键“的映射关系。常用方法:
-
put(K key, V value):添加新的键、值。如果值和已有键重复,会抛出异常
-
forcePut(K key, V value):添加新的键、值。如果值和已有键重复,会覆盖原来的键、值
-
inverse():得到”值-键“的BitMap对象
@Test
public void test4(){
BiMap<String,String> biMap= HashBiMap.create();
biMap.put("sina","sina.com");
biMap.put("qq","qq.com");
biMap.put("sina","sina.cn"); //会覆盖原来的value
System.out.println(biMap.inverse().get("qq.com"));
//biMap.put("tecent","qq.com"); //抛出异常
biMap.forcePut("tecent","qq.com"); //强制替换key
System.out.println(biMap.get("qq")); //通过value找key
System.out.println(biMap.inverse().get("qq.com"));
System.out.println(biMap.inverse().get("sina.com"));
System.out.println(biMap.inverse().inverse()==biMap);
}
输出:
qq
null
tecent
null
true
5. Table#
Table类似多个索引的表,类似 Map<R, Map<C, V>> 的数据结构。它有两个支持所有类型的键:”行”和”列”,可以通过以下方法获取多个视图:
- rowMap():用Map<R, Map<C, V>>表现Table<R, C, V>。同样的, rowKeySet()返回”行”的集合Set。
- row(r):用Map<C, V>返回给定”行”的所有列,对这个map进行的写操作也将写入Table中。
- cellSet():用元素类型为Table.Cell的Set表现Table<R, C, V>。Cell类似于Map.Entry,但它是用行和列两个键区分的。