Guava常用数据结构

Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。

Guava 的好处:

  • 标准化 - Guava库是由谷歌托管。
  • 高效 - 可靠,快速和有效的扩展JAVA标准库
  • 优化 -Guava库经过高度的优化。
  • 函数式编程 -增加JAVA功能和处理能力。
  • 实用程序 - 提供了经常需要在应用程序开发的许多实用程序类。
  • 验证 -提供标准的故障安全验证机制。
  • 最佳实践 - 强调最佳的做法。

今天我们就一起来学习一下Guava吧。

不可变集合

范例

public static final ImmutableSet COLOR_NAMES = ImmutableSet.of(
    "red",
    "orange",
    "yellow",
    "green",
    "blue",
    "purple");

class Foo {
    Set bars;
    Foo(Set 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 GOOGLE_COLORS =
ImmutableSet.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,键为元素,值为计数

Guava 的 Multiset API 也结合考虑了这两种方式:
当把 Multiset 看成普通的 Collection 时,它表现得就像无序的 ArrayList:

  • add(E) 添加单个给定元素
  • iterator() 返回一个迭代器,包含 Multiset 的所有元素(包括重复的元素)
  • size() 返回所有元素的总个数(包括重复的元素)
    当把 Multiset 看作 Map时,它也提供了符合性能期望的查询操作:
  • count(Object) 返回给定元素的计数。HashMultiset.count 的复杂度为 O(1),TreeMultiset.count 的复杂
    度为 O(log n)。
  • entrySet() 返回 Set,和 Map 的 entrySet 类似。
  • elementSet() 返回所有不重复元素的 Set,和 Map 的 keySet()类似。
  • 所有 Multiset 实现的内存消耗随着不重复元素的个数线性增长。

特别注意,Multi set.addAll(Collection)可以添加 Collection 中的所有元素并进行计数,这比用 for 循环往 Map 添加元素和计数 方便多了。

方法 描述
count(E) 给定元素在 Multiset 中的计数
elementSet() Multiset 中不重复元素的集合,类型为 Set
entrySet() 和 Map 的 entrySet 类似,返回 Set>,其中包含的 Entry 支持 getElement()和 getCount()方法
add(E, int) 增加给定元素在 Multiset 中的计数
remove(E, int) 减少给定元素在 Multiset 中的计数
setCount(E, int) 设置给定元素在 Multiset 中的计数,不可以为负数
size() 返回集合元素的总个数(包括重复的元素)

Multiset 不是 Map

请注意,Multiset不是 Map,虽然 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或 Map,并且要忍受这个结构的笨拙。例如,Map通常用来表示非标定有向图。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,让你可以按另一种方式看待 Multimap。重要的是,不会有任何键映射到空集合:一个键要么至少到一个值,要么根本就不在Multimap 中。

很少会直接使用 Multimap 接口,更多时候你会用 ListMultimapSetMultimap接口,它们分别把键映射到 List 或 Set。

修改 Multimap

Multimap.get(key)以集合形式返回键所对应的值视图,即使没有任何对应的值,也会返回空集合。ListMultimap.get(key)返回 List,SetMultimap.get(key)返回 Set。
对值视图集合进行的修改最终都会反映到底层的 Multimap。例如:

Set aliceChildren = childrenMultimap.get(alice);
aliceChildren.clear();
aliceChildren.add(bob);
aliceChildren.add(carol);

其他(更直接地)修改 Multimap 的方法有:

方法签名 描述 等价于
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)

Multimap 的视图

Multimap 还支持若干强大的视图:

  • asMap 为 Multimap提供 Map形式的视图。。返回的 Map 支持 remove 操作,并且会反映到底层的 Multimap,但它不支持 put 或 putAll 操作。
  • entries 用 Collection>返回 Multimap 中所有”键-单个值映射”——包括重复
    键。(对 SetMultimap,返回的是 Set)
  • keySet 用 Set 表示 Multimap 中所有不同的键。
  • keys 用 Multiset 表示 Multimap 中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个
    Multiset 中移除元素,但不能做添加操作;移除操作会反映到底层的 Multimap。
  • values() 用一个”扁平”的Collection包含 Multimap 中的所有值。

Multimap 的各种实现

Multimap 提供了多种形式的实现。在大多数要使用 Map的地方,你都可以使用它们:

实现 键行为类似 值行为类似
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 nameToId = Maps.newHashMap();
Map idToName = Maps.newHashMap();
nameToId.put("Bob", 42);
idToName.put(42, "Bob");
//如果"Bob"和42已经在map中了,会发生什么?
//如果我们忘了同步两个map,会有诡异的bug发生...

BiMap是特殊的 Map:

  • 可以用 inverse() 反转 BiMap的键值映射
  • 保证值是唯一的,因此 values()返回 Set 而不是普通的 Collection

在 BiMap 中,如果你想把键映射到已经存在的值,会抛出 IllegalArgumentException 异常。如果对特定值,你想要强制替换它的键,请使用 BiMap.forcePut(key, value)

BiMap 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 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>的实现,这种方式很丑陋,使用上也不友好。Guava 为此提供了新集合类型 Table,它有两个支持所有类型的键:”行”和”列”。Table 提供多种视图,以便你从各种角度使用它:

  • rowMap():用 Map>表现 Table。同样的, rowKeySet()返回”行”的集合S
    et。
  • row(r) :用 Map返回给定”行”的所有列,对这个 map 进行的写操作也将写入 Table 中。
  • 类似的列访问方法:columnMap()columnKeySet()column(c)。(基于列的访问会比基于的行访问稍
    微低效点)
  • cellSet():用元素类型为 Table.Cell的 Set 表现 Table。Cell 类似于 Map.Entry,但
    它是用行和列两个键区分的。

Table 有如下几种实现:

  • HashBasedTable:本质上用 HashMap>实现;
  • TreeBasedTable:本质上用 TreeMap>实现;
    -ImmutableTable:本质上用 ImmutableMap>实现;注:ImmutableTable对稀疏或密集的数据集都有优化。
  • ArrayTable:要求在构造时就指定行和列的大小,本质上由一个二维数组实现,以提升访问速度和密集 Table 的内存利用率。

ClassToInstanceMap

ClassToInstanceMap 是一种特殊的 Map:它的键是类型,而值是符合键所指类型的对象。
为了扩展 Map 接口,ClassToInstanceMap 额外声明了两个方法:T getInstance(Class)T putInstanc e(Class, T),从而避免强制类型转换,同时保证了类型安全。
ClassToInstanceMap 有唯一的泛型参数,通常称为 B,代表 Map 支持的所有类型的上界。例如:

ClassToInstanceMap numberDefaults=MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));

从技术上讲,从技术上讲,ClassToInstanceMap, B>——或者换句话说,是一个映射 B 的子类型到对应实例的 Map。这让 ClassToInstanceMap 包含的泛型声明有点令人困惑,但请记住 B 始终是 Map 所支持类型的上界——通常 B 就是 Object。
对于 ClassToInstanceMap,Guava 提供了两种有用的实现:MutableClassToInstanceMapImmutableClassToInstanceMap

RangeSet

RangeSet描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。例如:

RangeSet 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表现 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 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表现 RangeMap。这可以用来遍历 RangeMap。
  • subRangeMap(Range):用 RangeMap 类型返回 RangeMap 与给定 Range 的交集视图。这扩展了传统的 headMap、subMap 和 tailMap 操作。

你可能感兴趣的:(Guava常用数据结构)