004-guava 集合-新增集合类型-MultiSet, MultiMap, BiMap , Table, ClassToInstanceMap, RangeSe, RangeMap等
一、概述
Guava引入了很多JDK没有的、但明显有用的新集合类型。这些新类型是为了和JDK集合框架共存,而没有往JDK集合抽象中硬塞其他概念。作为一般规则,Guava集合非常精准地遵循了JDK接口契约。
二、使用
2.1、MultiSet[无序+可重复]-工具类Multisets
Guava提供了一个新集合类型 Multiset,它可以多次添加相等的元素。维基百科从数学角度这样定义Multiset:”集合[set]概念的延伸,它的元素可以重复出现…与集合[set]相同而与元组[tuple]相反的是,Multiset元素的顺序是无关紧要的:Multiset {a, a, b}和{a, b, a}是相等的”。——译者注:这里所说的集合[set]是数学上的概念,Multiset继承自JDK中的Collection接口,而不是Set接口,所以包含重复元素并没有违反原有的接口契约。
这个接口没有实现java.util.Set接口,Set接口规定里面是不能够放入重复的元素,如果放入重复元素会被覆盖掉的;然而Multiset接口却是可以放入重复元素的,Set接口中的元素是[1,2,3],Multiset中确可以[1✖️2,2✖️3,3✖️3]来表示多个相同的元素。
Guava提供了多种Multiset的实现,大致对应JDK中Map的各种实现:
Map | 对应的Multiset | 是否支持null元素 |
HashMap | HashMultiset | 是 |
TreeMap | TreeMultiset | 是(如果comparator支持的话) |
LinkedHashMap | LinkedHashMultiset | 是 |
ConcurrentHashMap | ConcurrentHashMultiset | 否 |
ImmutableMap | ImmutableMultiset | 否 |
使用
@Test public void testMultiSet(){ Multiset<String> multiset= HashMultiset.create(); multiset.add("aa"); multiset.add("bb"); multiset.add("cc",2); System.out.println(multiset);//[aa, bb, cc x 2] System.out.println(multiset.size()); //4 System.out.println(multiset.count("cc"));//2 multiset.setCount("bb",4); System.out.println(multiset);//[aa, bb x 4, cc x 2] }
2.2、SortedMultiset
SortedMultiset是Multiset 接口的变种,它支持高效地获取指定范围的子集。比方说,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()来统计你的站点中延迟在100毫秒以内的访问,然后把这个值和latencies.size()相比,以获取这个延迟水平在总体访问中的比例。
TreeMultiset实现SortedMultiset接口。在撰写本文档时,ImmutableSortedMultiset还在测试和GWT的兼容性。
2.3、MultiMap[key-value key可以重复 ]-工具类Multimaps
程序开发中使用Map<K, List<V>>或Map<K, Set<V>>,并且要忍受这个结构的笨拙。例如,Map<K, Set<V>>通常用来表示非标定有向图。Guava的 Multimap可以很容易地把一个键映射到多个值。换句话说,Multimap是把键映射到任意多个值的一般方式。
很少会直接使用Multimap接口,更多时候你会用ListMultimap或SetMultimap接口,它们分别把键映射到List或Set。
Multimap提供了多种形式的实现。在大多数要使用Map<K, Collection<V>>的地方,你都可以使用它们:
实现 | 键行为类似 | 值行为类似 |
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,以最小化开销)
如果你想要更大的定制化,请用Multimaps.newMultimap(Map, Supplier<Collection>)或list和 set版本,使用自定义的Collection、List或Set实现Multimap。
示例:
@Test public void testMultiMap() { Multimap<String, String> multimap = ArrayListMultimap.create(); multimap.put("fruit", "bannana"); multimap.put("fruit", "apple");//key可以重复 multimap.put("fruit", "apple");//value可以重复,不会覆盖之前的 multimap.put("fruit", "peach"); multimap.put("fish", "crucian");//欧洲鲫鱼 multimap.put("fish", "carp");//鲤鱼 Collection<String> fruits = multimap.get("fruit"); System.err.println(fruits);//[bannana, apple, apple, peach] //对比 HashMultimap Multimap<String,String> multimap2= HashMultimap.create(); multimap2.put("fruit2", "bannana"); multimap2.put("fruit2", "apple"); multimap2.put("fruit2", "apple"); System.err.println(multimap2.size());//2 System.err.println(multimap2.get("fruit2"));//[apple, bannana] 注意: 这里只有一个apple }
2.4、BiMap[双向Map(Bidirectional Map) 键与值都不能重复]
传统上,实现键值对的双向映射需要维护两个单独的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)。
@Test public void testBiMap() { BiMap<String, Integer> userId = HashBiMap.create(); userId.put("lhx",30); userId.put("zll",28); String userForId = userId.inverse().get(30); System.out.println(userForId);//lhx userId.put("jm",30);//报错 String userForId2 = userId.inverse().get(30); System.out.println(userForId2);//lhx }
BiMap的各种实现
键–值实现 | 值–键实现 | 对应的BiMap实现 |
HashMap | HashMap | HashBiMap |
ImmutableMap | ImmutableMap | ImmutableBiMap |
EnumMap | EnumMap | EnumBiMap |
EnumMap | HashMap | EnumHashBiMap |
注:Maps类中还有一些诸如synchronizedBiMap的BiMap工具方法.
2.5、Table【双键的Map Map--> Table-->rowKey+columnKey+value //和sql中的联合主键有点像】-工具类Tables
行、列、值。当使用多个键做索引的时候,可能会用类似Map<FirstName, Map<LastName, Person>>的实现,这种方式很丑陋,使用上也不友好。
Guava为此提供了新集合类型Table,它有两个支持所有类型的键:”行”和”列”。
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了解详情。
示例
@Test public void testTable() { Table<String, String, Integer> table = HashBasedTable.create(); table.put("a", "b", 4); table.put("a", "c", 20); table.put("b", "c", 5); Map<String, Integer> a = table.row("a");// returns a Map mapping {b=4, c=20} System.out.println(a); Map<String, Integer> column = table.column("c");// returns a Map mapping {a=20, b=5} System.out.println(column); Integer integer = table.get("a", "c"); System.out.println(integer); //20 }
2.6、ClassToInstanceMap
ClassToInstanceMap是一种特殊的Map:它的键是类型,而值是符合键所指类型的对象。
为了扩展Map接口,ClassToInstanceMap额外声明了两个方法:T getInstance(Class<T>) 和T putInstance(Class<T>, T),从而避免强制类型转换,同时保证了类型安全。
ClassToInstanceMap有唯一的泛型参数,通常称为B,代表Map支持的所有类型的上界。例如:
class Person{ private String name; public Person(String name) { this.name = name; } public String getName() { return name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } } @Test public void testClassToInstanceMap() { ClassToInstanceMap<Person> instanceMap=MutableClassToInstanceMap.create(); instanceMap.putInstance(Person.class, new Person("lhx")); instanceMap.putInstance(Person.class, new Person("lhx2")); Person person = instanceMap.get(Person.class); // Person{name='lhx2'} 存储了 后一个 System.out.println(person); }
从技术上讲,ClassToInstanceMap<B>实现了Map<Class<? extends B>, B>——或者换句话说,是一个映射B的子类型到对应实例的Map。这让ClassToInstanceMap包含的泛型声明有点令人困惑,但请记住B始终是Map所支持类型的上界——通常B就是Object。
对于ClassToInstanceMap,Guava提供了两种有用的实现:MutableClassToInstanceMap和 ImmutableClassToInstanceMap。
2.7、RangeSet
描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。例如:
@Test public void testRangeSet() { 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)} System.out.println(rangeSet);//[[1..5], [10..10], [11..20)] }
请注意,要合并Range.closed(1, 10)和Range.closedOpen(11, 15)这样的区间,你需要首先用Range.canonical(DiscreteDomain)对区间进行预处理,例如DiscreteDomain.integers()。
注:RangeSet不支持GWT,也不支持JDK5和更早版本;因为,RangeSet需要充分利用JDK6中NavigableMap的特性。
2.8、RangeMap
RangeMap描述了”不相交的、非空的区间”到特定值的映射。和RangeSet不同,RangeMap不会合并相邻的映射,即便相邻的区间映射到相同的值。例如:
@Test public void testRangeMap() { 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"} System.out.println(rangeMap);//[[1..3]=foo, (3..5)=bar, (11..20)=foo] }
方式