Guava学习笔记2-集合

Posted by AceKei on October 18, 2018

新集合类型

Multiset

Guava提供了一个新集合类型 Multiset,它可以多次添加相等的元素。维基百科从数学角度这样定义Multiset:“集合[set]概念的延伸,它的元素可以重复出现…与集合[set]相同而与元组[tuple]相反的是,Multiset元素的顺序是无关紧要的:Multiset {a, a, b}和{a, b, a}是相等的”。
——译者注:这里所说的集合[set]是数学上的概念,Multiset继承自JDK中的Collection接口,而不是Set接口,所以包含重复元素并没有违反原有的接口契约。

方法 描述
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() 返回集合元素的总个数(包括重复的元素)

Multiset不是Map
请注意,Multiset不是Map<E, Integer>,虽然Map可能是某些Multiset实现的一部分。准确来说Multiset是一种Collection类型,并履行了Collection接口相关的契约。关于Multiset和Map的显著区别还包括:

  • Multiset中的元素计数只能是正数。任何元素的计数都不能为负,也不能是0。elementSet()和entrySet()视图中也不会有这样的元素。
  • 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

SortedMultiset
SortedMultiset是Multiset 接口的变种,它支持高效地获取指定范围的子集。比方说,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()来统计你的站点中延迟在100毫秒以内的访问,然后把这个值和latencies.size()相比,以获取这个延迟水平在总体访问中的比例。

Multimap

每个有经验的Java程序员都在某处实现过Map<K, List>或Map<K, Set>,并且要忍受这个结构的笨拙。例如,Map<K, Set>通常用来表示非标定有向图。Guava的 Multimap可以很容易地把一个键映射到多个值。换句话说,Multimap是把键映射到任意多个值的一般方式。

可以用两种方式思考Multimap的概念:”键-单个值映射”的集合:

1
a -> 1 a -> 2 a ->4 b -> 3 c -> 5

或者”键-值集合映射”的映射:

1
a -> [1, 2, 4] b -> 3 c -> 5

一般来说,Multimap接口应该用第一种方式看待,但asMap()视图返回Map<K, Collection>,让你可以按另一种方式看待Multimap。重要的是,不会有任何键映射到空集合:一个键要么至少到一个值,要么根本就不在Multimap中。

很少会直接使用Multimap接口,更多时候你会用ListMultimapSetMultimap接口,它们分别把键映射到List或Set。 修改Multimap
Multimap.get(key)以集合形式返回键所对应的值视图,即使没有任何对应的值,也会返回空集合。ListMultimap.get(key)返回List,SetMultimap.get(key)返回Set。

对值视图集合进行的修改最终都会反映到底层的Multimap。例如:

1
2
3
4
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)| 依次添加键到多个值的映射| 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的视图

  • asMap为Multimap<K, V>提供Map<K,Collection>形式的视图。返回的Map支持remove操作,并且会反映到底层的Multimap,但它不支持put或putAll操作。更重要的是,如果你想为Multimap中没有的键返回null,而不是一个新的、可写的空集合,你就可以使用asMap().get(key)。(你可以并且应当把asMap.get(key)返回的结果转化为适当的集合类型——如SetMultimap.asMap.get(key)的结果转为Set,ListMultimap.asMap.get(key)的结果转为List——Java类型系统不允许ListMultimap直接为asMap.get(key)返回List——译者注:也可以用Multimaps中的asMap静态方法帮你完成类型转换)
  • entries用Collection<Map.Entry<K, V»返回Multimap中所有”键-单个值映射”——包括重复键。(对SetMultimap,返回的是Set)
  • keySet用Set表示Multimap中所有不同的键。
  • keys用Multiset表示Multimap中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个Multiset中移除元素,但不能做添加操作;移除操作会反映到底层的Multimap。
  • values()用一个”扁平”的Collection包含Multimap中的所有值。这有一点类似于Iterables.concat(multimap.asMap().values()),但它直接返回了单个Collection,而不像multimap.asMap().values()那样是按键区分开的Collection。

Multimap不是Map
Multimap<K, V>不是Map<K,Collection>,虽然某些Multimap实现中可能使用了map。它们之间的显著区别包括:

  • Multimap.get(key)总是返回非null、但是可能空的集合。这并不意味着Multimap为相应的键花费内存创建了集合,而只是提供一个集合视图方便你为键增加映射值——译者注:如果有这样的键,返回的集合只是包装了Multimap中已有的集合;如果没有这样的键,返回的空集合也只是持有Multimap引用的栈对象,让你可以用来操作底层的Multimap。因此,返回的集合不会占据太多内存,数据实际上还是存放在Multimap中。
  • 如果你更喜欢像Map那样,为Multimap中没有的键返回null,请使用asMap()视图获取一个Map<K, Collection>。(或者用静态方法Multimaps.asMap()为ListMultimap返回一个Map<K, List>。对于SetMultimap和SortedSetMultimap,也有类似的静态方法存在)
  • 当且仅当有值映射到键时,Multimap.containsKey(key)才会返回true。尤其需要注意的是,如果键k之前映射过一个或多个值,但它们都被移除后,Multimap.containsKey(key)会返回false。
  • Multimap.entries()返回Multimap中所有”键-单个值映射”——包括重复键。如果你想要得到所有”键-值集合映射”,请使用asMap().entrySet()。
  • Multimap.size()返回所有”键-单个值映射”的个数,而非不同键的个数。要得到不同键的个数,请改用Multimap.keySet().size()。 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()保留了所有键和值的迭代顺序。详情见doc链接。
  • **LinkedHashMultimap保留了映射项的插入顺序,包括键插入的顺序,以及键映射的所有值的插入顺序。
  • 请注意,并非所有的Multimap都和上面列出的一样,使用Map<K, Collection>来实现(特别是,一些Multimap实现用了自定义的hashTable,以最小化开销)
  • 如果你想要更大的定制化,请用Multimaps.newMultimap(Map, Supplier)或list和 set版本,使用自定义的Collection、List或Set实现Multimap。

Table

1
2
3
4
5
6
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()返回”行”的集合Set
  • 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的内存利用率。ArrayTable与其他Table的工作原理有点不同,请参见Javadoc了解详情。

强大的集合工具类:Collections中未包含的集合工具

我们用相对直观的方式把工具类与特定集合接口的对应关系归纳如下:

集合接口 属于JDK还是Guava 对应的Guava工具类
Collection JDK Collections2:不要和Collections混淆
List JDK Lists
Set JDK Sets
SortedSet JDK Sets
Map JDK Maps
SortedMap JDK Maps
Queue JDK Queues
Multiset Guava Multisets
Multimap Guava Multimaps
BiMap Guava Maps
Table Guava Tables

静态工厂方法

1
2
//声明对象
Multiset<String> multiset = HashMultiset.create();

Iterables

在可能的情况下,Guava提供的工具方法更偏向于接受Iterable而不是Collection类型。在Google,对于不存放在主存的集合——比如从数据库或其他数据中心收集的结果集,因为实际上还没有攫取全部数据,这类结果集都不能支持类似[size()]的操作 ——通常都不会用Collection类型来表示。
因此,很多你期望的支持所有集合的操作都在Iterables类中。大多数Iterables方法有一个在Iterators类中的对应版本,用来处理Iterator。

Lists

| 方法 | 描述| |—-|—-| partition(List, int) | 把List按指定大小分割 reverse(List) | 返回给定List的反转视图。注: 如果List是不可变的,考虑改用ImmutableList.reverse()。

举例说明:

1
2
3
List countUp = Ints.asList(1, 2, 3, 4, 5);
List countDown = Lists.reverse(theList); // {5, 4, 3, 2, 1}
List<List> parts = Lists.partition(countUp, 2);//[[1,2}, {3,4}, {5]]
静态工厂方法
具体实现类型 工厂方法
ArrayList basic, with elements, from Iterable, with exact capacity, with expected size, from Iterator
LinkedList basic, from Iterable

Sets

集合理论方法

我们提供了很多标准的集合运算(Set-Theoretic)方法,这些方法接受Set参数并返回SetView,可用于:

  • 直接当作Set使用,因为SetView也实现了Set接口;
  • 用copyInto(Set)拷贝进另一个可变集合;
  • 用immutableCopy()对自己做不可变拷贝。
方法
union(Set, Set)
intersection(Set, Set)
difference(Set, Set)
symmetricDifference(Set, Set)

举例说明:

1
2
3
4
5
Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
SetView<String> intersection = Sets.intersection(primes,wordsWithPrimeLength);
// intersection包含"two", "three", "seven"
return intersection.immutableCopy();//可以使用交集,但不可变拷贝的读取效率更高
其他Set工具方法

|方法| 描述| 另请参见| |—|—|—| cartesianProduct(List) |返回所有集合的笛卡儿积| cartesianProduct(Set...) powerSet(Set) |返回给定集合的所有子集 |

举例说明:

1
2
3
4
5
6
Set<String> animals = ImmutableSet.of("gerbil", "hamster");
Set<String> fruits = ImmutableSet.of("apple", "orange", "banana");
Set<List<String>> product = Sets.cartesianProduct(animals, fruits);
// [["gerbil", "apple"}, {"gerbil", "orange"}, {"gerbil", "banana"},{"hamster", "apple"}, {"hamster", "orange"}, {"hamster", "banana"]]
Set<Set<String>> animalSets = Sets.powerSet(animals);
// [[}, {"gerbil"}, {"hamster"}, {"gerbil", "hamster"]]
静态工厂方法

Sets提供如下静态工厂方法:

具体实现类型 工厂方法
HashSet basic, with elements, from Iterable, with expected size, from Iterator
LinkedHashSet basic, from Iterable, with expected size
TreeSet basic, with Comparator, from Iterable

Maps

Maps类有若干值得单独说明的、很酷的方法。

difference

Maps.difference(Map, Map)用来比较两个Map以获取所有不同点。该方法返回MapDifference对象,把不同点的维恩图分解为:

方法 说明
entriesInCommon() 两个Map中都有的映射项,包括匹配的键与值
entriesDiffering() 键相同但是值不同值映射项。返回的Map的值类型为MapDifference.ValueDifference,以表示左右两个不同的值
entriesOnlyOnLeft() 键只存在于左边Map的映射项
entriesOnlyOnRight() 键只存在于右边Map的映射项

举例说明:

1
2
3
4
5
6
7
8
9
10
Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
Map<String, Integer> right = ImmutableMap.of("a", 1, "b", 5, "c", 6);
MapDifference<String, Integer> diff = Maps.difference(left, right);
Map<String, Integer> result;
result = diff.entriesInCommon(); // {"b" => 2}
System.out.println(result);
result = diff.entriesOnlyOnLeft(); // {"a" => 1}
System.out.println(result);
result = diff.entriesOnlyOnRight(); // {"d" => 5}
System.out.println(result);
静态工厂方法

Maps提供如下静态工厂方法:

具体实现类型 工厂方法
HashMap basic, from Map, with expected size
LinkedHashMap basic, from Map
TreeMap basic, from Comparator, from SortedMap
EnumMap from Class, from Map
ConcurrentMap:支持所有操作 basic
IdentityHashMap basic

Multisets

标准的Collection操作会忽略Multiset重复元素的个数,而只关心元素是否存在于Multiset中,如containsAll方法。为此,Multisets提供了若干方法,以顾及Multiset元素的重复性:

方法 说明 和Collection方法的区别
containsOccurrences(Multiset sup, Multiset sub) 对任意o,如果sub.count(o)<=super.count(o),返回true Collection.containsAll忽略个数,而只关心sub的元素是否都在super中
removeOccurrences(Multiset removeFrom, Multiset toRemove) 对toRemove中的重复元素,仅在removeFrom中删除相同个数。 Collection.removeAll移除所有出现在toRemove的元素
retainOccurrences(Multiset removeFrom, Multiset toRetain) 修改removeFrom,以保证任意o都符合removeFrom.count(o)<=toRetain.count(o) Collection.retainAll保留所有出现在toRetain的元素
intersection(Multiset, Multiset) 返回两个multiset的交集; 没有类似方法

Multimaps

index

作为Maps.uniqueIndex的兄弟方法,Multimaps.index(Iterable, Function)通常针对的场景是:有一组对象,它们有共同的特定属性,我们希望按照这个属性的值查询对象,但属性值不一定是独一无二的。

比方说,我们想把字符串按长度分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ImmutableSet digits = ImmutableSet.of("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine");
Function<String, Integer> lengthFunction = new Function<String, Integer>() {
    public Integer apply(String string) {
        return string.length();
    }
};
 
ImmutableListMultimap<Integer, String> digitsByLength= Multimaps.index(digits, lengthFunction);
/*
*  digitsByLength maps:
*  3 => {"one", "two", "six"}
*  4 => {"zero", "four", "five", "nine"}
*  5 => {"three", "seven", "eight"}
*/
invertFrom

反转Multimap。Guava 提供了invertFrom(Multimap toInvert, Multimap dest)做这个操作,并且你可以自由选择反转后的Multimap实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ArrayListMultimap<String, Integer> multimap = ArrayListMultimap.create();
multimap.putAll("b", Ints.asList(2, 4, 6));
multimap.putAll("a", Ints.asList(4, 2, 1));
multimap.putAll("c", Ints.asList(2, 5, 3));
TreeMultimap<Integer, String> inverse = Multimaps.invertFrom(multimap, TreeMultimap.<Integer, String>create());
System.out.println(inverse);
//注意我们选择的实现,因为选了TreeMultimap,得到的反转结果是有序的
/*
 * inverse maps:
 *  1 => {"a"}
 *  2 => {"a", "b", "c"}
 *  3 => {"c"}
 *  4 => {"a", "b"}
 *  5 => {"c"}
 *  6 => {"b"}
 */
forMap

想在Map对象上使用Multimap的方法吗?forMap(Map)把Map包装成SetMultimap。这个方法特别有用,例如,与Multimaps.invertFrom结合使用,可以把多对一的Map反转为一对多的Multimap。

1
2
3
4
5
6
Map<String, Integer> map = ImmutableMap.of("a", 1, "b", 1, "c", 2);
SetMultimap<String, Integer> multimap = Multimaps.forMap(map);
// multimap:["a" => {1}, "b" => {1}, "c" => {2}]
Multimap<Integer, String> inverse = Multimaps.invertFrom(multimap, HashMultimap.<Integer, String>create());
System.out.println(inverse);
// inverse:[1 => {"a","b"}, 2 => {"c"}]

Tables

自定义Table

堪比Multimaps.newXXXMultimap(Map, Supplier)工具方法,Tables.newCustomTable(Map, Supplier<Map>)允许你指定Table用什么样的map实现行和列。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用LinkedHashMaps替代HashMaps
Table<String, Character, Integer> table = Tables.newCustomTable(Maps.<String, Map<Character, Integer>>newLinkedHashMap(),
        new Supplier<Map<Character, Integer>>() {
            @Override
            public Map<Character, Integer> get() {
                return Maps.newLinkedHashMap();
            }
        });
table.put("a", 'z', 1);
table.put("b", 'x', 2);
table.put("c", 'y', 3);
table.put("d", 's', 4);
System.out.println(table);
transpose

transpose(Table<R, C, V>)方法允许你把Table<C, R, V>转置成Table<R, C, V>。例如,如果你在用Table构建加权有向图,这个方法就可以把有向图反转。

1
Table transpose = Tables.transpose(table);