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得到的,所以应该没有bug
    public 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();
        // top3
        Comparator<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:

  1. 其是完全的纯函数,没有中间状态,不依赖外部。
  2. 返回类型是不可变类型,防止他人修改。
  3. 所有存在的计数都是正确的,没有计数为0的情况
  4. 方法的入参是接口,出参是接口,我们也不知道返回的对象具体是哪个实现类👍

初学者遇到的一个重要的问题:Collectors从哪里找

  1. 集合接口对应的工具类里找:
  • List -> Lists
  • Set -> Sets
  • Map -> Maps
  • Multiset -> Multisets
  • BiMap -> BiMaps
  • RangeMap -> RangeMaps
  1. 从不可变类型中找
    因为Collector返回的对象就应该是不可变类型的,一个stream流的计算可以理解为一个函数,其独立实现了一个功能。
  • List -> ImmutableList
  • Set -> ImmutableSet
  • Map -> ImmutableMap
  • BiMap -> ImmutableBiMap
  • Multiset -> ImmutableMultiset
  • Multimap -> ImmutableMultimap
  • RangeMap -> ImmutableRangeMap

再来看看 Guava 中不可变集合类型的好处:

  • 保证了集合浅层的不可变性,不可新增、删除、替换
  • 创建是不支持空对象,避免了无谓的空指针检查
  • 线程安全👍
  • 不可被继承,防止逻辑被修改,保证了其行为的一致性
  • asList 方法直接转换为列表形式,方便
    底层高效实现,不占用额外的空间
  • 可以建立多种视图 view: subList, elementSet, entrySet
  • 工具类支持多种运算,比如进行集合间运算
  • 开发者遵循规范的话,不用空指针检查,因为默认不可变类型就是非空的,空集合不用null表示,所以可以直接调用isEmpty等方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mldxxxxll5

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值