Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。
Guava 的好处:
- 标准化 - Guava库是由谷歌托管。
- 高效 - 可靠,快速和有效的扩展JAVA标准库
- 优化 -Guava库经过高度的优化。
- 函数式编程 -增加JAVA功能和处理能力。
- 实用程序 - 提供了经常需要在应用程序开发的许多实用程序类。
- 验证 -提供标准的故障安全验证机制。
- 最佳实践 - 强调最佳的做法。
今天我们就一起来学习一下Guava吧。
不可变集合
范例
public static final ImmutableSet<String> COLOR_NAMES = ImmutableSet.of(
"red",
"orange",
"yellow",
"green",
"blue",
"purple");
class Foo {
Set<Bar> bars;
Foo(Set<Bar> bars) {
this.bars = ImmutableSet.copyOf(bars); // defensive copy!
}
}
为什么要使用不可变集合
不可变对象有很多优点,包括:
- 当对象被不可信的库调用时,不可变形式是安全的;
- 不可变对象被多个线程调用时,不存在竞态条件问题
- 不可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
- 不可变对象因为有固定不变,可以作为常量来安全使用。
创建对象的不可变拷贝是一项很好的防御性编程技巧。Guava 为所有 JDK 标准集合类型和 Guava 新集合类型都提供了简单易用的不可变版本。
JDK 也提供了 Collections.unmodifiableXXX 方法把集合包装为不可变形式,但我们认为不够好:
- 笨重而且累赘:不能舒适地用在所有想做防御性拷贝的场景;
- 不安全:要保证没人通过原集合的引用进行修改,返回的集合才是事实上不可变的;
- 低效:包装过的集合仍然保有可变集合的开销,比如并发修改的检查、散列表的额外空间,等等。
如果你没有修改某个集合的需求,或者希望某个集合保持不变时,把它防御性地拷贝到不可变集合是个很好的实践。
重要提示:所有 Guava 不可变集合的实现都不接受 null 值。我们对 Google 内部的代码库做过详细研究,发现只有 5%的情况需要在集合中允许 null 元素,剩下的 95%场景都是遇到 null 值就快速失败。如果你需要在不可变集合中使用 null,请使用 JDK 中的Collections.unmodifiableXXX 方法。更多细节建议请参考“使用和避免null”。
怎么使用不可变集合
不可变集合可以用如下多种方式创建:
- 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();
此外,对有序不可变集合来说,排序是在构造集合的时候完成的,如:
ImmutableSortedSet.of("a", "b", "c", "a", "d", "b");
会在构造时就把元素排序为 a, b, c, d。
asList视图
所有不可变集合都有一个 asList()方法提供 ImmutableList 视图,来帮助你用列表形式方便地读取集合元素。例如,你可以使用 sortedSet.asList().get(k)从 ImmutableSortedSet 中读取第 k 个最小元素。
asList()返回的 ImmutableList 通常是——并不总是——开销稳定的视图实现,而不是简单地把元素拷贝进 List。也就是说,asList 返回的列表视图通常比一般的列表平均性能更好,比如,在底层集合支持的情况下,它总是使用高效的 contains 方法。
关联可变集合和不可变集合
可变集合接口 | 属于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 |
新集合类型
Guava 引入了很多 JDK 没有的、但我们发现明显有用的新集合类型。这些新类型是为了和 JDK 集合框架共存,而没有往 JDK 集合抽象中硬塞其他概念。作为一般规则,Guava 集合非常精准地遵循了 JDK 接口契约。
Multiset
Guava 提供了一个新集合类型 Multiset,它可以多次添加相等的元素。Multiset继承自 JDK 中的 Collection 接口,而不是 Set 接口,所以包含重复元素并没有违反原有的接口契约。
可以用两种方式看待 Multiset:
- 没有元素顺序限制的 ArrayList
- Map<E, Integer>,键为元素,值为计数
Guava 的 Multiset API 也结合考虑了这两种方式:
当把 Multiset 看成普通的 Collection 时,它表现得就像无序的 ArrayList:
-
add(E)
添加单个给定元素 -
iterator()
返回一个迭代器,包含 Multiset 的所有元素(包括重复的元素) -
size()
返回所有元素的总个数(包括重复的元素)
当把 Multiset 看作 Map<E, Integer>时,它也提供了符合性能期望的查询操作: -
count(Object)
返回给定元素的计数。HashMultiset.count 的复杂度为 O(1),TreeMultiset.count 的复杂
度为 O(log n)。 -
entrySet()
返回 Set<Multiset.Entry>,和 Map 的 entrySet 类似。 -
elementSet()
返回所有不重复元素的 Set,和 Map 的 keySet()类似。 - 所有 Multiset 实现的内存消耗随着不重复元素的个数线性增长。
特别注意,Multi set.addAll(Collection)可以添加 Collection 中的所有元素并进行计数,这比用 for 循环往 Map 添加元素和计数 方便多了。
方法 | 描述 |
---|---|
count(E) | 给定元素在 Multiset 中的计数 |
elementSet() | Multiset 中不重复元素的集合,类型为 Set<E> |
entrySet() | 和 Map 的 entrySet 类似,返回 Set<Multiset.Entry<E>>,其中包含的 Entry 支持 getElement()和 getCount()方法 |
add(E, int) | 增加给定元素在 Multiset 中的计数 |
remove(E, int) | 减少给定元素在 Multiset 中的计数 |
setCount(E, int) | 设置给定元素在 Multiset 中的计数,不可以为负数 |
size() | 返回集合元素的总个数(包括重复的元素) |
Multiset 不是 Map
请注意,Multiset不是 Map<E, Integer>,虽然 Map 可能是某些 Multiset 实现的一部分。准确来说 Multiset 是一种 Collection 类型,并履行了 Collection 接口相关的契约。关于 Multiset 和 Map 的显著区别还包括:
- Multiset 中的元素计数只能是正数。任何元素的计数都不能为负,也不能是 0。elementSet()和 entrySe
t()视图中也不会有这样的元素。 - multiset.size()返回集合的大小,等同于所有元素计数的总和。对于不重复元素的个数,应使用 elementSet().size()方法。(因此,add(E)把 multiset.size()增加 1)
- multiset.iterator()会迭代重复元素,因此迭代长度等于 multiset.size()。
- Multiset 支持直接增加、减少或设置元素的计数。setCount(elem, 0)等同于移除所有 elem。
- 对 multiset 中没有的元素,multiset.count(elem)始终返回 0。
Multiset 的各种实现
Guava 提供了多种 Multiset 的实现,大致对应 JDK 中 Map 的各种实现:
Map | 对应的Multiset | 是否支持null元素 |
---|---|---|
HashMap | HashMultiset | 是 |
TreeMap | TreeMultiset | 是(如果 comparator 支持的话) |
LinkedHashMap | LinkedHashMultiset | 是 |
ConcurrentHashMap | ConcurrentHashMultiset | 否 |
ImmutableMap | ImmutableMultiset | 否 |
Multimap
每个有经验的 Java 程序员都在某处实现过 Map<K, List>或 Map<K, Set>,并且要忍受这个结构的笨拙。例如,Map<K, Set>通常用来表示非标定有向图。Guava 的 Multimap 可以很容易地把一个键映射到多个值。换句话说,Multimap 是把键映射到任意多个值的一般方式
。
可以用两种方式思考 Multimap 的概念:”键-单个值映射”的集合:
a -> 1 a -> 2 a ->4 b -> 3 c -> 5
或者”键-值集合映射”的映射:
a -> [1, 2, 4] b -> 3 c -> 5
一般来说,Multimap 接口应该用第一种方式看待,但 asMap()
视图返回 Map<K, Collection>,让你可以按另一种方式看待 Multimap。重要的是,不会有任何键映射到空集合:一个键要么至少到一个值,要么根本就不在Multimap 中。
很少会直接使用 Multimap 接口,更多时候你会用 ListMultimap
或SetMultimap
接口,它们分别把键映射到 List 或 Set。
修改 Multimap
Multimap.get(key)以集合形式返回键所对应的值视图,即使没有任何对应的值,也会返回空集合。ListMultimap.get(key)返回 List,SetMultimap.get(key)返回 Set。
对值视图集合进行的修改最终都会反映到底层的 Multimap。例如:
Set<Person> aliceChildren = childrenMultimap.get(alice);
aliceChildren.clear();
aliceChildren.add(bob);
aliceChildren.add(carol);
其他(更直接地)修改 Multimap 的方法有:
方法签名 | 描述 | 等价于 |
---|---|---|
put(K, V) | 添加键到单个值的映射 | multimap.get(key).add(value) |
putAll(K, Iterable<V>) | 依次添加键到多个值的映射 | 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<V>) | 清除键对应的所有值,并重新把 key 关联到 Iterable 中的每个元素。返回的集合包含所有之前映射到 K 的值。 | multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values) |
Multimap 的视图
Multimap 还支持若干强大的视图:
-
asMap
为 Multimap<K, V>提供 Map<K,Collection>形式的视图。。返回的 Map 支持 remove 操作,并且会反映到底层的 Multimap,但它不支持 put 或 putAll 操作。 -
entries
用 Collection<Map.Entry<K, V>>返回 Multimap 中所有”键-单个值映射”——包括重复
键。(对 SetMultimap,返回的是 Set) -
keySet
用 Set 表示 Multimap 中所有不同的键。 -
keys
用 Multiset 表示 Multimap 中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个
Multiset 中移除元素,但不能做添加操作;移除操作会反映到底层的 Multimap。 -
values()
用一个”扁平”的Collection包含 Multimap 中的所有值。
Multimap 的各种实现
Multimap 提供了多种形式的实现。在大多数要使用 Map<K, Collection>的地方,你都可以使用它们:
实现 | 键行为类似 | 值行为类似 |
---|---|---|
ArrayListMultimap | HashMap | ArrayList |
HashMultimap | HashMap | HashSet |
LinkedListMultimap* | LinkedHashMap* | LinkedList* |
LinkedHashMultimap** | LinkedHashMap | LinkedHashMap |
TreeMultimap | TreeMap | TreeSet |
ImmutableListMultimap | ImmutableMap | ImmutableList |
ImmutableSetMultimap | ImmutableMap | ImmutableSet |
除了两个不可变形式的实现,其他所有实现都支持 null 键和 null 值
- LinkedListMultimap.entries()保留了所有键和值的迭代顺序。
- LinkedHashMultimap 保留了映射项的插入顺序,包括键插入的顺序,以及键映射的所有值的插入顺序。
BiMap
传统上,实现键值对的双向映射
需要维护两个单独的 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)
。
BiMap<String, Integer> userId = HashBiMap.create();
...
String userForId = userId.inverse().get(id);
BiMap 的各种实现
键–值实现 | 值–键实现 | 对应的BiMap实现 |
---|---|---|
HashMap | HashMap | HashBiMap |
ImmutableMap | ImmutableMap | ImmutableBiMap |
EnumMap | EnumMap | EnumBiMap |
EnumMap | HashMap | EnumHashBiMap |
注:Maps 类中还有一些诸如 synchronizedBiMap 的 BiMap 工具方法.
Table
Table<Vertex, Vertex, Double> weightedGraph = HashBasedTable.create();
weightedGraph.put(v1, v2, 4);
weightedGraph.put(v1, v3, 20);
weightedGraph.put(v2, v3, 5);
weightedGraph.row(v1); // returns a Map mapping v2 to 4, v3 to 20
weightedGraph.column(v3); // returns a Map mapping v1 to 20, v2 to 5
通常来说,当你想使用多个键做索引的时候,你可能会用类似 Map<FirstName, Map<LastName, Person>>的实现,这种方式很丑陋,使用上也不友好。Guava 为此提供了新集合类型 Table,它有两个支持所有类型的键:”行”和”列”。Table 提供多种视图,以便你从各种角度使用它:
-
rowMap()
:用 Map<R, Map<C, V>>表现 Table<R, C, V>。同样的, rowKeySet()返回”行”的集合S
et。 -
row(r)
:用 Map<C, V>返回给定”行”的所有列,对这个 map 进行的写操作也将写入 Table 中。 - 类似的列访问方法:
columnMap()
、columnKeySet()
、column(c)
。(基于列的访问会比基于的行访问稍
微低效点) -
cellSet()
:用元素类型为 Table.Cell<R, C, V>的 Set 表现 Table<R, C, V>。Cell 类似于 Map.Entry,但
它是用行和列两个键区分的。
Table 有如下几种实现:
-
HashBasedTable
:本质上用 HashMap<R, HashMap<C, V>>实现; -
TreeBasedTable
:本质上用 TreeMap<R, TreeMap<C,V>>实现;
-ImmutableTable
:本质上用 ImmutableMap<R, ImmutableMap<C, V>>实现;注:ImmutableTable对稀疏或密集的数据集都有优化。 -
ArrayTable
:要求在构造时就指定行和列的大小,本质上由一个二维数组实现,以提升访问速度和密集 Table 的内存利用率。
ClassToInstanceMap
ClassToInstanceMap 是一种特殊的 Map:它的键是类型,而值是符合键所指类型的对象。
为了扩展 Map 接口,ClassToInstanceMap 额外声明了两个方法:T getInstance(Class)
和 T putInstanc e(Class, T)
,从而避免强制类型转换,同时保证了类型安全。
ClassToInstanceMap 有唯一的泛型参数,通常称为 B,代表 Map 支持的所有类型的上界。例如:
ClassToInstanceMap<Number> numberDefaults=MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));
从技术上讲,从技术上讲,ClassToInstanceMap<B> 实现了 Map<Class<? extends B>, B>——或者换句话说,是一个映射 B 的子类型到对应实例的 Map。这让 ClassToInstanceMap 包含的泛型声明有点令人困惑,但请记住 B 始终是 Map 所支持类型的上界——通常 B 就是 Object。
对于 ClassToInstanceMap,Guava 提供了两种有用的实现:MutableClassToInstanceMap
和 ImmutableClassToInstanceMap
。
RangeSet
RangeSet描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。例如:
RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1,10]}
rangeSet.add(Range.closedOpen(11, 15));//不相连区间:{[1,10], [11,15)}
rangeSet.add(Range.closedOpen(15, 20)); //相连区间; {[1,10], [11,20)}
rangeSet.add(Range.openClosed(0, 0)); //空区间; {[1,10], [11,20)}
rangeSet.remove(Range.open(5, 10)); //分割[1, 10]; {[1,5], [10,10], [11,20)}
请注意,要合并 Range.closed(1, 10)和 Range.closedOpen(11, 15)这样的区间,你需要首先用 Range.canonical(DiscreteDomain)对区间进行预处理,例如 DiscreteDomain.integers()。
RangeSet 的视图
RangeSet 的实现支持非常广泛的视图:
-
complement()
:返回 RangeSet 的补集视图。complement 也是 RangeSet 类型,包含了不相连的、非空的区间。 -
subRangeSet(Range)
:返回 RangeSet 与给定 Range 的交集视图。这扩展了传统排序集合中的 headSet、subSet 和 tailSet 操作。 -
asRanges()
:用 Set<Range>表现 RangeSet,这样可以遍历其中的 Range。 -
asSet(DiscreteDomain)
(仅 ImmutableRangeSet 支持):用 ImmutableSortedSet表现 RangeSet,以区间中所有元素的形式而不是区间本身的形式查看。(这个操作不支持 DiscreteDomain 和 RangeSet 都没有上边界,或都没有下边界的情况)
RangeSet 的查询方法
为了方便操作,RangeSet 直接提供了若干查询方法,其中最突出的有:
-
contains(C)
:RangeSet 最基本的操作,判断 RangeSet 中是否有任何区间包含给定元素。 -
rangeContaining(C)
:返回包含给定元素的区间;若没有这样的区间,则返回 null。 -
encloses(Range)
:简单明了,判断 RangeSet 中是否有任何区间包括给定区间。 -
span()
:返回包括 RangeSet 中所有区间的最小区间。
RangeMap
RangeMap 描述了”不相交的、非空的区间”到特定值的映射。和 RangeSet 不同,RangeMap 不会合并相邻的映射,即便相邻的区间映射到相同的值
。例如:
RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); //{[1,10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo"}
rangeMap.put(Range.open(10, 20), "foo"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo", (10,20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); //{[1,3] => "foo", (3,5) => "bar", (11,20) => "foo"}
RangeMap 的视图
RangeMap 提供两个视图:
-
asMapOfRanges()
:用 Map<Range, V>表现 RangeMap。这可以用来遍历 RangeMap。 -
subRangeMap(Range)
:用 RangeMap 类型返回 RangeMap 与给定 Range 的交集视图。这扩展了传统的 headMap、subMap 和 tailMap 操作。