跟着 Guava 学 Java 之 集合工具类

1,683 阅读8分钟

背景

先来回顾一下 JDK 的 Collectionsjava.util.Collections 提供了一系列静态方法,能更方便地操作各种集合。

比如:

  • 创建空集合 Collections.emptyList();
  • 创建单元素集合 Collections.singletonList("apple");
  • 排序 Collections.sort(list);
  • 创建不可变集合 Collections.unmodifiableList(mutable);
  • 创建线程安全集合 Collections.synchronizedList(list);
  • ......

Guava 沿着 Collections 的思路 提供了 更多的工具方法,适用于所有集合的静态方法,使之成为更强大的集合工具类。

Guava 提供的集合工具不止是对 Collections 的扩展和增强,还包括了其他集合类型的工具类,我们把工具类与特定集合接口的对应关系归纳如下:

InterfaceJDK or Guava?对应 Guava 工具类
CollectionJDKCollections2
ListJDKLists
SetJDKSets
SortedSetJDKSets
MapJDKMaps
SortedMapJDKMaps
QueueJDKQueues
MultisetGuavaMultisets
MultimapGuavaMultimaps
BiMapGuavaMaps
TableGuavaTables

静态构造器

在 JDK 7 之前,构造新的范型集合时要讨厌地重复声明范型:

List<TypeThatsTooLongForItsOwnGood> list = new ArrayList<TypeThatsTooLongForItsOwnGood>();

JDK 7 以后因为有了钻石操作符(Diamond Operator)可以自动推断参数类型,所以省点儿事儿

List<TypeThatsTooLongForItsOwnGood> list = new ArrayList<>();

用 Guava 可以这样写:

List<TypeThatsTooLongForItsOwnGood> list = Lists.newArrayList();

你可能觉得:这没什么牛的呀,跟 JDK7 以后没啥区别呀,人家还是原生的。

是的,没错,尤其是你用 JDK 9 以后的版本,JDK 从功能上跟 Guava 就基本一样了,举个例子,比如带元素初始化:

List<String> theseElements = Lists.newArrayList("alpha", "beta", "gamma");

上面这行是利用了 Guava 的 Lists ,JDK 7 没有比这行代码更好的方法,但 JDK9 人家有,比如:

List<String> theseElements2 = List.of("alpha", "beta", "gamma");

所以我们说,跟着 Guava 学 Java,随着版本的迭代,你觉得哪个好用,哪个适合你用哪个,我的重要是把这里面的知识点讲清楚。

我们再来看个例子:创建集合时指定初始化集合大小:

List<Type> exactly100 = Lists.newArrayListWithCapacity(100);

你可能说,哥们,这 JDK 有啊,这不多此一举吗?

ArrayList<Object> objects = new ArrayList<>(10);

是的,作用一样,但 Guava 的做法,可以利用工厂方法名称,提高代码的可读性,你不觉得虽然名字长了,但可读性比 JDK 那个好很多吗?代码不止是写给机器的,也是写给人看的,不能指望团队里所有人都是强者。代码可读性越高,越健壮越容易维护。

Iterables

Iterables 类包含了一系列的静态方法,来操作或返回 Iterable 对象

看几个常用的方法:

方法描述
concat(Iterable)串联多个 iterables 的懒视图
frequency(Iterable, Object)返回对象在 iterable 中出现的次数
partition(Iterable, int)把 iterable 按指定大小分割,得到的子集都不能进行修改操作
getFirst(Iterable, T default)返回 iterable 的第一个元素,若 iterable 为空则返回默认值
getLast(Iterable)返回 iterable 的最后一个元素,若 iterable 为空则抛出 NoSuchElementException
elementsEqual(Iterable, Iterable)如果两个 iterable 中的所有元素相等且顺序一致,返回 true
unmodifiableIterable(Iterable)返回 iterable 的不可变视图
limit(Iterable, int)最多返回指定数量的元素
getOnlyElement(Iterable)获取 iterable 中唯一的元素,如果 iterable 为空或有多个元素,则快速失败

对于上面这些常用的方法,可能你觉得使用 JDK8 以后的 Stream 一行也都搞定了,是的,还是那句话,Guava 是个工具,尤其在 JDK8 之前用来增强 API 很好用,但工具不止一个,Java 也在发展,有些东西就变成可选项了,看你的需求和习惯使用。Guava 也有对应的流式风格的工具类,比如 FluentIterable

Lists

除了静态工厂方法和函数式编程方法,Lists为 List 类型的对象提供了若干工具方法。

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

Sets

Sets工具类包含了若干好用的方法。

Sets 中 为我们提供了很多标准的集合运算(Set-Theoretic)方法。比如我们常用的集合的 “交、并、差集” 以及对称差集

交集

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<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
SetView<String> union = Sets.union(primes,wordsWithPrimeLength);

// union 包含 [two, three, five, seven, one, six, eight]
return intersection.immutableCopy();

差集

Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
SetView<String> difference = Sets.union(primes,wordsWithPrimeLength);

// difference 包含 “five”
return difference.immutableCopy();

对称差集

Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
SetView<String> symmetricDifference = Sets.union(primes,wordsWithPrimeLength);

// symmetricDifference 包含 [five, one, six, eight]
return symmetricDifference.immutableCopy();

注意返回的都是 SetView ,它可以:

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

笛卡儿积

方法描述
cartesianProduct(List<Set>)返回所有集合的笛卡儿积
powerSet(Set)返回给定集合的所有子集
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"}}

Maps

Maps 有若干很酷的方法。

uniqueIndex

有一组对象,它们在某个属性上分别有独一无二的值,而我们希望能够按照这个属性值查找对象。

比方说,我们有一堆字符串,这些字符串的长度都是独一无二的,而我们希望能够按照特定长度查找字符串:

ImmutableMap<Integer, String> stringsByIndex = Maps.uniqueIndex(strings,
    new Function<String, Integer> () {
        public Integer apply(String string) {
            return string.length();
        }
 });

你可以想到了,我们常见的场景还有 把一个 List 的对象集合转成 Map,map 的 key 通常是对象的 ID。用 uniqueIndex 方法可以这样写:

 Map<Integer, Animal> map = Maps.uniqueIndex(list, Animal::getId);

或者你用 Java8 的 Stream 也一样:

  ArrayList<Animal> animals = Lists.newArrayList(new Animal(1L, "Dog"), new Animal(2L, "Cat"));
  //下面两种写法都可以
  Map<Long, Animal> map = animals.stream().collect(Collectors.toMap(Animal::getId, Function.identity()));
  Map<Long, Animal> map = animals.stream().collect(Collectors.toMap(Animal::getId, animal -> animal));

注意:key 要是唯一的,否则会报错。

difference

找不同,对比两个 map,告诉你哪里不同

Maps.difference(Map, Map)用来比较两个 Map 以获取所有不同点。该方法返回 MapDifference 对象

下面是 MapDifference 的一些方法:

entriesInCommon()两个 Map 中都有的映射项,包括匹配的键与值
entriesDiffering()键相同但是值不同值映射项。返回的 Map 的值类型为MapDifference.ValueDifference,以表示左右两个不同的值
entriesOnlyOnLeft()键只存在于左边 Map 的映射项
entriesOnlyOnRight()键只存在于右边 Map 的映射项
Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
Map<String, Integer> right = ImmutableMap.of("b", 2, "c", 4, "d", 5);
MapDifference<String, Integer> diff = Maps.difference(left, right);

diff.entriesInCommon(); // {"b" => 2}
diff.entriesDiffering(); // {"c" => (3, 4)}
diff.entriesOnlyOnLeft(); // {"a" => 1}
diff.entriesOnlyOnRight(); // {"d" => 5}

看到这个你能想到什么? 我举个场景:审计日志或操作日志,谁什么时间做了什么,数据从旧值变更为新值,这些要记录下来

是不是可以用上面这个 Maps 的方法? 适合不适合你自己决定,这里是提供个思路。

MultiSets

下面要介绍的工具类都是新集合类型的工具类,比如 MultiSet 和 MultiMap 之类的,有关这些 Guava 的新集合类型,在之前的文章 《跟着 Guava 学 Java 之 新集合类型》 都有介绍,有不清楚的可以再翻回去看一看。

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

方法说明**和 Collection **方法的区别
containsOccurrences(Multiset sup, Multiset sub)对任意 o,如果 sub.count(o)<=super.count(o),返回 trueCollection.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 的交集;没有类似方法

举例来说:

Multiset<String> multiset1 = HashMultiset.create();
multiset1.add("a", 2);

Multiset<String> multiset2 = HashMultiset.create();
multiset2.add("a", 5);

multiset1.containsAll(multiset2); //返回 true;因为包含了所有不重复元素,
//虽然 multiset1 实际上包含 2 个"a",而 multiset2 包含 5 个"a"
Multisets.containsOccurrences(multiset1, multiset2); // returns false

multiset2.removeOccurrences(multiset1); // multiset2 现在包含 3 个"a"
multiset2.removeAll(multiset1);//multiset2 移除所有"a",虽然 multiset1 只有 2 个"a"
multiset2.isEmpty(); // returns true

Multisets 中的其他工具方法还包括:

copyHighestCountFirst(Multiset)返回 Multiset 的不可变拷贝,并将元素按重复出现的次数做降序排列
unmodifiableMultiset(Multiset)返回 Multiset 的只读视图
unmodifiableSortedMultiset(SortedMultiset)返回 SortedMultiset 的只读视图
Multiset<String> multiset = HashMultiset.create();
multiset.add("a", 3);
multiset.add("b", 5);
multiset.add("c", 1);

ImmutableMultiset highestCountFirst = Multisets.copyHighestCountFirst(multiset);
//highestCountFirst,包括它的 entrySet 和 elementSet,按{"b", "a", "c"}排列元素

Multimaps

index

Multimaps 的 index 方法跟前面介绍的 Maps.uniqueIndex 方法是兄弟方法。与 uniqueIndex 方法相反,通常针对的场景是:有一组对象,它们有共同的特定属性,我们希望按照这个属性的值查询对象,但属性值不一定是独一无二的。比方说,我们想把字符串按长度分组:

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 可以把多个键映射到同一个值,也可以把一个键映射到多个值,反转 Multimap 也会很有用。Guava 提供了invertFrom(Multimap toInvert, Multimap dest)做这个操作,并且你可以自由选择反转后的 Multimap 实现。

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<String, Integer>.create());
//注意我们选择的实现,因为选了 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。

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());
// inverse:[1 => {"a","b"}, 2 => {"c"}] 

参考