Guava 用法指南
Guava是优秀程序员的必经之路,作为谷歌推出的类库,Guava提供了Java标准类库之外的其他支持,比如不可变集合、更丰富的集合类型、限流器、常用的工具类、消息订阅支持等等。
不过除了一些经常使用的特殊用途的类,如单机限流的时候会用 RateLimiter,大部分时间可能会把Guava优秀的类库设计给遗忘,我们可能会用Map<T, Integer>来对对象进行计数,而不是用更好用且不易出bug的Multiset。
究其原因,大部分程序对于标准库的使用极其熟练,但是对于新的类库,即使是知道有这个类库的存在,可能也失去了学习新类库的动力,因为毕竟使用标准库也可以实现相同的目标,同时由于对标准库的熟悉,也在心理上不害怕出现未知的bug。
还有一个重要的原因是大家并不熟悉Guava的一些设计思想,所以想使用的时候就会遇到门槛。对于初学者,可能连创建对象都是问题。如对对象进行分类计数,我们可能想到了使用Multiset: 但是Guava对于标准库中没有的接口及其实现类,并没有new Xxx()这样public 的构造器。究其原因,对于对象的创建,Guava提倡使用静态方法(create…)、构造者模式、Collector等方式创建。
实际上,Java的集合类库的设计并不能算特别优秀(比如不支持不可变类型),使用Guava集合支持类写出的代码可读性好,更可以避免bug的出现,这些优势我会在博客中依次分析。
中文互联网上对于Guava的介绍有些浮于表面,会让人觉得Guava只是一个可有可无的类库,实际上,Guava代表着优秀的软件设计思想,对于提高代码能力大有裨益。作为Guava编程思想的拥趸者之一,我在工作中一直使用着Guava,并且体会着优秀代码带给人的快乐,我以后会陆续发文介绍Guava背后的优秀设计。
学习Guava最好的材料当然就是官方文档,看官方文档可以快速入门。要理解Guava中集合类的设计,也可以看看 EffectiveJava 这本书,我之前写过短评说过,这本书可以说是一份Java最佳实践和闭坑指南。
从计数谈起
import com.google.common.collect.*;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;import static com.google.common.collect.Comparators.greatest;
import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.*;record Student(Long id, String sid, String name, Long classId) {}
record SchoolClass(Long classId, String name) {}class CreationDemo {// 统计每个班级的学生人数// bad code,很多初学者会写这样的代码// 重要的编程思想:不要重复,代码能复用就复用// 虽然没有错,但容易出错// ps:这段代码实际上是将merge方法inline得到的,所以应该没有bugpublic static Map<Long, Integer> countByClass(List<Student> students) {Map<Long, Integer> countByClass = new HashMap<>();for (Student student : students) {Long key = student.classId();Integer oldValue = countByClass.get(key);Integer newValue = (oldValue == null) ? 1 : oldValue + 1;countByClass.put(key, newValue);}return countByClass;}// 使用 Map.merge 方法计数, map 依然具有多种状态,为可变类型public static Map<Long, Integer> countByClass0(List<Student> students) {Map<Long, Integer> countByClass = new HashMap<>();for (Student student : students) {countByClass.merge(student.classId(), 1, Integer::sum);}return countByClass;}// 使用 Collector 计数,避免了中间状态,不易出错,纯函数public static Map<Long, Long> countByClass1(List<Student> students) {return students.stream().collect(groupingBy(Student::classId, counting()));}// 使用 Multiset 计数// 其有两个基本实现类,和Java类似,起名也类似: HashMultiset, TreeMultiset// 如果你查看Multiset的子类的话,会看到很多类,不过大部分我们都用不到,因为其只是实现,访问权限为包访问权限,也不需要了解// 软件设计的原则:关注抽象(接口),而非实现(实现类)public static Multiset<Long> countByClass2(List<Student> students) {Multiset<Long> countByClass = HashMultiset.create();for (Student student : students) {countByClass.add(student.classId());}return countByClass;}// 最佳实践// 屏蔽了实现,保证了代码的正确性(不易出错)// 返回对象值为不可变类型,给你安全感,不用怕后续别人的修改// 纯函数// 所有的 Collector 都应该 import static, 1. 提高可读性 2. 避免重复表达public static ImmutableMultiset<Long> countByClass3(List<Student> students) {return students.stream().collect(toImmutableMultiset(Student::classId, it -> 1));}// 使用示例public static void consumeMultiset(ImmutableMultiset<Long> countByClass) {// 不可变类型大多数都可以直接转换为 List,可以方便地传给其他方法// 比如数据库的查询 xxxRepo.listBySchoolClass(List<Long> classes)ImmutableList<Long> classesAsList = countByClass.asList();// 获取班级集合ImmutableSet<Long> classes = countByClass.elementSet();// top3Comparator<Multiset.Entry<Long>> top3Collector = comparingInt(Multiset.Entry::getCount);ImmutableList<Long> top3 = countByClass.entrySet().stream().collect(collectingAndThen(greatest(3, top3Collector), CreationDemo::mapToElements));}private static ImmutableList<Long> mapToElements(List<Multiset.Entry<Long>> entries) {return entries.stream().map(Multiset.Entry::getElement).collect(ImmutableList.toImmutableList());}
}
对于以上代码分析,比较得到了最佳实践:countByClass3:
- 其是完全的纯函数,没有中间状态,不依赖外部。
- 返回类型是不可变类型,防止他人修改。
- 所有存在的计数都是正确的,没有计数为0的情况
- 方法的入参是接口,出参是接口,我们也不知道返回的对象具体是哪个实现类👍
初学者遇到的一个重要的问题:Collectors从哪里找
- 集合接口对应的工具类里找:
- List -> Lists
- Set -> Sets
- Map -> Maps
- Multiset -> Multisets
- BiMap -> BiMaps
- RangeMap -> RangeMaps
- 从不可变类型中找
因为Collector返回的对象就应该是不可变类型的,一个stream流的计算可以理解为一个函数,其独立实现了一个功能。
- List -> ImmutableList
- Set -> ImmutableSet
- Map -> ImmutableMap
- BiMap -> ImmutableBiMap
- Multiset -> ImmutableMultiset
- Multimap -> ImmutableMultimap
- RangeMap -> ImmutableRangeMap
再来看看 Guava 中不可变集合类型的好处:
- 保证了集合浅层的不可变性,不可新增、删除、替换
- 创建是不支持空对象,避免了无谓的空指针检查
- 线程安全👍
- 不可被继承,防止逻辑被修改,保证了其行为的一致性
- asList 方法直接转换为列表形式,方便
底层高效实现,不占用额外的空间 - 可以建立多种视图 view: subList, elementSet, entrySet
- 工具类支持多种运算,比如进行集合间运算
- 开发者遵循规范的话,不用空指针检查,因为默认不可变类型就是非空的,空集合不用null表示,所以可以直接调用isEmpty等方法