深入Guava集合操作
在Java开发中,Google Guava库是处理集合的强大工具。起源于Google内部需求,Guava以简洁性、性能优化为理念,提供高效不可变集合和实用工具类。本文深入剖析Guava的核心功能,为开发者呈现集合操作的全新视角,无论经验水平,都能获得实用技巧和深刻见解。
一、不可变集合
1、为什么使用不可变集合
不可变对象有很多优点,包括:
- 当对象被不可信的库调用时,不可变形式是安全的;
- 不可变对象被多个线程调用时,不存在竞态条件问题
- 可变集合不需要考虑变化,因此可以节省时间和空间。所有不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
- 不可变对象因为有固定不变,可以作为常量来安全使用。
2、创建不可变集合的方式:
- copyOf方法,如ImmutableSet.copyOf(set);
- of方法,如ImmutableSet.of(“a”, “b”, “c”)或 ImmutableMap.of(“a”, 1, “b”, 2);
- Builder工具,如:
private static final ImmutableSet<String> SET =
ImmutableSet.<String>builder()
.add("a","b")
.addAll(Lists.newArrayList("c","d"))
.build();
此外,对有序不可变集合来说,排序是在构造集合的时候完成的,如: ImmutableSortedSet.of("a", "b", "c", "a", "d", "b");
会在构造时就把元素排序为a, b, c, d。
3、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 |
三、新集合类型
1、Multiset
Multiset可以多次添加相等元素,集合[set]概念的延伸,它的元素可以重复出现…与集合[set]相同而与元组[tuple]相反的是,Multiset元素的顺序是无关紧要的:Multiset {a, a, b}和{a, b, a}是相等的
可以用两种方式看待Multiset:
- 没有元素顺序限制的ArrayList
- Map<E, Integer>,键为元素,值为计数
(1)、常见方法
方法 | 描述 |
---|---|
int count(E) | 给定元素在Multiset中的计数 |
Set<E> elementSet() | Multiset中不重复元素的集合,类型为Set<E> |
Set<Multiset.Entry<E>> entrySet() | 和Map的entrySet类似,返回Set<Multiset.Entry<E>>,其中包含的Entry支持getElement()和getCount()方法 |
int add(E, int) | 增加给定元素在Multiset中的计数 |
boolean add(E element) | 增加一个指定的元素到multiset |
boolean contains(E element) | 判断此多集中是否包含指定的元素 |
boolean containsAll(Collection<?> elements) | 判断此多集至少包含一个出现指定集合的所有元素 |
remove(E, int) | 减少给定元素在Multiset中的计数,删除指定元素 |
removeAll(Collection<?> c) | 删除包含在指定集合中的元素 |
boolean retainAll(Collection<?> e) | 保持包含指定集合中的元素 |
int setCount(E, int) | 设置给定元素在Multiset中的计数,不可以为负数,添加/删除指定元素,使其达到所期望的元素个数 |
int size() | 返回集合元素的总个数(包括重复的元素) |
Iterator iterator() | 返回一个迭代器,包含Multiset的所有元素(包括重复的元素) |
(2)、示例
/**
* MultiSet
*/
@Test
public void multiSetTest(){
Multiset<String> multiset = HashMultiset.create();
List<String> list = Lists.newArrayList("a","b","c","d","a","c","d","a","d","a");
multiset.addAll(list);
System.out.println("a的个数:"+multiset.count("a"));
System.out.println("multiset的个数:"+multiset.size());
Set<String> set = multiset.elementSet();
System.out.println("不重复元素:"+ Joiner.on(",").join(set));
Iterator<String> iterator = multiset.iterator();
System.out.println("multiset元素:"+Joiner.on(",").join(iterator));
Set<Multiset.Entry<String>> entrySet = multiset.entrySet();
Map<String,Integer> setMap = Maps.newHashMap();
entrySet.forEach(e -> {
setMap.put(e.getElement(),e.getCount());
});
System.out.println("元素详情:"+Joiner.on(";").withKeyValueSeparator("=").join(setMap));
multiset.remove("a",2);
System.out.println("删除a后,a的个数:"+multiset.count("a"));
System.out.println("是否包含List:"+multiset.containsAll(Lists.newArrayList("a","c")));
System.out.println("是否包含List:"+multiset.containsAll(Lists.newArrayList("a","c","e")));
}
(3)、SortedMultiset
SortedMultiset是Multiset 接口的变种,它支持高效地获取指定范围的子集
2、MultiMap
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。
(1)、常用方法
方法 | 描述 | 等价于 |
---|---|---|
boolean put(K, V) | 添加键到单个值的映射 | multimap.get(key).add(value) |
boolean 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) |
Map<K,Collection<V>> asMap() | 获取MultiMap的视图,键值K,以及K对应的集合 | |
void clear() | 清除所有的键值对 | |
boolean containsEntry(Object key,Object value) | 判断是否包含key-value对应的键值对 | |
boolean containsKey(Object key) | 判断是否包含键值key | |
boolean containsValue(Object value) | 判断是否包含值value | |
Collection<Map.Entry<K,V>> entries() | MultiMap为Map<Entry>情况下,返回所有的键值对集合 | |
Collection<V> get(K k) | 返回键k对应的所有集合 | |
boolean isEmpty() | 判断MultiMap是否是空,即不包含键值对 | |
MultiSet<K> keys() | 返回所有的键值K,包含重复 | |
Set<K> keySet() | 返回所有的键值K,不重复 | |
int size() | 返回键值对的数量 | |
Collection<V> values | 返回所有的value |
(2)、示例
/**
* MultiMap
*/
@Test
public void multiMapTest(){
Multimap<String,String> multimap = HashMultimap.create();
multimap.putAll("lower",Lists.newArrayList("a","b","c","d"));
multimap.putAll("upper",Lists.newArrayList("A","B","C","D"));
Map<String, Collection<String>> asMap = multimap.asMap();
System.out.println("asMap视图:"+Joiner.on(";").withKeyValueSeparator("=").join(asMap));
Multiset<String> multisetKey = multimap.keys();
System.out.println("所有的key:"+Joiner.on(",").join(multisetKey.iterator()));
Set<String> keySet = multimap.keySet();
System.out.println("不重复的key:"+Joiner.on(",").join(keySet));
System.out.println("lower:"+Joiner.on(",").join(multimap.get("lower")));
multimap.put("lower","e");
System.out.println("添加后的lower:"+Joiner.on(",").join(multimap.get("lower")));
System.out.println("upper:"+Joiner.on(",").join(multimap.get("upper")));
multimap.remove("upper","D");
System.out.println("移除元素后的upper:"+Joiner.on(",").join(multimap.get("upper")));
System.out.println("是否包含lower-b:"+multimap.containsEntry("lower","b"));
System.out.println("是否包含lower-b:"+multimap.containsEntry("lower","f"));
System.out.println("是否包含key(upper):"+multimap.containsKey("upper"));
System.out.println("是否包含value(c):"+multimap.containsValue("c"));
Collection<Map.Entry<String,String>> collection = multimap.entries();
System.out.println("MultiMap详情:"+Joiner.on(";").withKeyValueSeparator("=").join(collection));
Collection<String> values = multimap.values();
System.out.println("MultiMap所有的value:"+Joiner.on(",").join(values));
}
(3)、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<V>>。(或者用静态方法Multimaps.asMap()为ListMultimap返回一个Map<K, List<V>>。对于SetMultimap和SortedSetMultimap,也有类似的静态方法存在)
-
当且仅当有值映射到键时,Multimap.containsKey(key)才会返回true。尤其需要注意的是,如果键k之前映射过一个或多个值,但它们都被移除后,Multimap.containsKey(key)会返回false。
-
Multimap.entries()返回Multimap中所有”键-单个值映射”——包括重复键。如果你想要得到所有”键-值集合映射”,请使用asMap().entrySet()。
-
Multimap.size()返回所有”键-单个值映射”的个数,而非不同键的个数。要得到不同键的个数,请改用Multimap.keySet().size()。
(4)、Multimap的各种实现
实现 | 键行为类似 | 值行为类似 |
---|---|---|
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<V>>来实现(特别是,一些Multimap实现用了自定义的hashTable,以最小化开销)
3、BiMap
BiMap<K, V>是特殊的Map:
- 可以用 inverse()反转BiMap<K, V>的键值映射
- 保证值是唯一的,因此 values()返回Set而不是普通的Collection
在BiMap中,如果你想把键映射到已经存在的值,会抛出IllegalArgumentException异常。
(1)、常用方法
方法 | 描述 |
---|---|
V forcePut(String key, V value) | 对于特定的值,强制替换它的键 |
BiMap<K,V> inverse() | k-v键值对的转换,即v-k |
V put<K key,V value> | 关联v到k |
void putAll(Map<? extend k,? extend V> map) | 将map加入到BiMap |
Set values() | 返回BiMap映射中包含的Collection视图 |
(2)、BiMap的各种实现
键–值实现 | 值–键实现 | 对应的BiMap实现 |
---|---|---|
HashMap | HashMap | HashBiMap |
ImmutableMap | ImmutableMap | ImmutableBiMap |
EnumMap | EnumMap | EnumBiMap |
EnumMap | HashMap | EnumHashBiMap |
(3)、示例
/**
* BiMap
*/
@Test
public void biMapTest(){
BiMap<String, String> biMap = HashBiMap.create();
biMap.putAll(ImmutableMap.of("a","1","b","2","c","3","d","4","e","5"));
System.out.println("所有的值:"+Joiner.on(",").join(biMap.values()));
System.out.println("转换后所有的值:"+Joiner.on(",").join(biMap.inverse().values()));
String v = biMap.forcePut("a","10");
System.out.println("替换的值:"+v);
System.out.println("所有的值:"+Joiner.on(",").join(biMap.values()));
}
4、Table
Table是Guava提供的一个接口 Interface Table<R,C,V>,由rowKey+columnKey+value组成 它有两个键,一个值,和一个n行三列的数据表类似,n行取决于Table对对象中存储了多少个数据。
(1)、常用方法
方法 | 描述 |
---|---|
Set<Table.Cell<R,C,V>> cellSet() | 返回集合中的行键,列键,值三元组 |
void clear() | 清除所有的键值对 |
Map<R,V> column(C columnKey) | 获取列键对应的键值对 |
Map<C,V> row(R row) | 获取行键对应的列以及值 |
Set<C> columnKeySet() | 获取所有的列键 |
Set<R> rowKeySet() | 获取行键 |
Map<C,Map<R,V>> columnMap | 返回列键对应的行键-值的视图 |
boolean contains(Object rowKey,Object columnKey) | 判断是否包含指定的行键,列键 |
boolean containsColumn(Object columnKey) | 判断是否包含指定的列键 |
boolean containsRow(Object rowKey) | 判断是否包含指定的行键 |
boolean containsValue(Object value) | 判断是否包含值 |
V get(Object rowKey,Object columnKey) | 返回指定的行键,列键对应的值,不存在则返回null |
boolean isEmpty() | 判断集合是否为空 |
V put(Object rowKey,Object columnKey,Object value) | put值 |
void putAll(Table<? extend R,? extend C,? extend V> table) | put指定的table |
V remove(Object rowKey,Object columnKey) | 如果有,则移除指定行键,列键 |
Map<R,Map<C,V>> rowMap() | 获取每个行键对应的列键,值的视图 |
int size() | 集合的个数(行键/列键/值) |
Collection<V> values() | 集合值的集合,包括重复的 |
(2)、示例
/**
* Table
*/
@Test
public void tableTest(){
Table<String,String,Integer> table = HashBasedTable.create();
table.put("grade_1","class_1",100);
table.put("grade_1","class_2",95);
table.put("grade_1","class_3",80);
table.put("grade_2","class_1",88);
table.put("grade_2","class_2",95);
table.put("grade_2","class_3",99);
table.put("grade_2","class_3",100);
Set<Table.Cell<String,String,Integer>> cellSet = table.cellSet();
cellSet.forEach(cell -> {
System.out.println("table中的行:"+cell.getRowKey()+";列:"+cell.getColumnKey()+";值:"+cell.getValue());
});
System.out.println("grade1对应的class:"+Joiner.on(";").withKeyValueSeparator("=").join(table.row("grade_1")));
System.out.println("class1对应的grade:"+Joiner.on(";").withKeyValueSeparator("=").join(table.column("class_1")));
System.out.println("所有的grade:"+Joiner.on(",").join(table.rowKeySet()));
System.out.println("所有的class:"+Joiner.on(",").join(table.columnKeySet()));
Map<String,Map<String,Integer>> rowMap = table.rowMap();
rowMap.forEach((row,map) -> {
System.out.println(row +"行对应的列值:"+Joiner.on(";").withKeyValueSeparator("=").join(map));
});
Map<String,Map<String,Integer>> columnMap = table.columnMap();
columnMap.forEach((column,map) -> {
System.out.println(column +"列对应的行值:"+Joiner.on(";").withKeyValueSeparator("=").join(map));
});
System.out.println("是否包含grade_1 和 class_2:"+table.contains("grade_1","class_2"));
table.remove("grade_1","class_2");
System.out.println("是否包含grade_1 和 class_2:"+table.contains("grade_1","class_2"));
}
(3)、Table有如下几种实现:
-
HashBasedTable:本质上用HashMap<R, HashMap<C, V>>实现;
-
TreeBasedTable:本质上用TreeMap<R, TreeMap<C,V>>实现;
-
ImmutableTable:本质上用ImmutableMap<R, ImmutableMap<C, V>>实现;注:ImmutableTable对稀疏或密集的数据集都有优化。
-
ArrayTable:要求在构造时就指定行和列的大小,本质上由一个二维数组实现,以提升访问速度和密集Table的内存利用率。ArrayTable与其他Table的工作原理有点不同。
5、ClassToInstanceMap
ClassToInstanceMap是一种特殊的Map:它的键是类型,而值是符合键所指类型的对象。
为了扩展Map接口,ClassToInstanceMap额外声明了两个方法:T getInstance(Class T) 和T putInstance(Class , T),从而避免强制类型转换,同时保证了类型安全。
ClassToInstanceMap有唯一的泛型参数,通常称为B,代表Map支持的所有类型的上界。
对于ClassToInstanceMap,Guava提供了两种有用的实现:MutableClassToInstanceMap和 ImmutableClassToInstanceMap。
示例
/**
* ClassToInstanceMap
*/
@Test
public void classToInstanceMapTest(){
ClassToInstanceMap<Number> instanceMap = MutableClassToInstanceMap.create();
instanceMap.putInstance(Integer.class,123);
instanceMap.putInstance(Long.class,456L);
instanceMap.putInstance(Double.class,789.09);
System.out.println("Integer:"+instanceMap.getInstance(Integer.class));
System.out.println("Long:"+instanceMap.getInstance(Long.class));
System.out.println("Double:"+instanceMap.getInstance(Double.class));
}
6、RangSet
RangeSet描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。
结论
通过深入探索Google Guava库的集合操作,我们不仅仅发现了一个功能强大的工具,更是领略到了一个高效、简洁的Java编程理念。Guava不仅提供了基础数据结构,还为开发者提供了一整套处理集合的利器,从不可变集合到高效工具类,无一不展现出其设计的巧妙之处。
在实际项目中,Guava为我们提供了更清晰、更简单的集合操作方式,帮助我们避免了许多常见的错误和异常。它的性能优化更是让我们在处理大规模数据时事半功倍。
作为Java开发者,我们应该充分了解并灵活运用Guava库,以提高代码的可读性、可维护性和性能。无论是新手还是老手,Guava都能为我们的开发工作带来便捷和效率。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等