Java谷歌工具库Guava

guava就是google used java,它是一个谷歌持续维护的一个核心的java工具包,囊括了从新集合类型、不可变集合、图结构处理、并发工具、I/O、哈希、缓存等等各个方面的工具。它被各个公司各大项目广泛使用,可以说,几乎有java出现的地方就有它的身影。

事实上,guava已经成了JDK以外的又一个标准,guava里涵盖的很多东西,最终都被添加到了jdk中,如函数式编程、Stream流、Optional类、不可变集合API等等。

无论从哪个角度,guava都是值得java程序员深入学习的一个工具库。

guava的思维导图:

Java谷歌工具库Guava插图

1、集合框架

1.1、集合工具类

// 创建一个ArrayList,可以传不定长参数
List<Integer> arrayList = Lists.newArrayList(1, 2, 3);

// 创建一个LinkedList,传一个iterable(可迭代)变量
List<Integer> linkedList = Lists.newLinkedList(arrayList);

// 翻转一个集合,原集合不变
List<Integer> reversed = Lists.reverse(arrayList);

// 集合变换,传一个function对象,相当于stream操作
List<Integer> transformed = Lists.transform(arrayList, i -> i * 2);

// 创建一个HashSet
Set<Integer> hashSet = Sets.newHashSet(1, 2, 3);

// 创建一个TreeSet
Set<Integer> treeSet = Sets.newTreeSet(Sets.newHashSet(4, 2, 3));

// 求两个Set的并集,返回的是一个只读的视图
Sets.SetView<Integer> union = Sets.union(hashSet, treeSet);

// 求两个Set的交集,返回的是一个只读的视图
Sets.SetView<Integer> intersection = Sets.intersection(hashSet, treeSet);

// 求两个Set的差集,返回的是一个只读的视图
Sets.SetView<Integer> difference = Sets.difference(hashSet, treeSet);

// jdk9创建不可变map的api
Map<String, Object> map0 = Map.of("a", 1, "b", 2, "c", 3);
Map<String, Object> map1 = Map.of("a", 1, "b", 2, "d", 4);

// 求两个map的差异
MapDifference<String, Object> mapDifference = Maps.difference(map0, map1);

1.2、扩展集合

主要扩展了Multiset(背包)和Multimap(多值映射)

// Multiset是支持重复元素的集合,即背包
// 创建背包
Multiset<String> multiset = HashMultiset.create();

// 向背包中添加一个元素
multiset.add("one");

// 向背包中添加多个相同元素
multiset.add("two", 4);

// 查询元素个数
int oneCount = multiset.count("one");

// 删除一个元素若干次
multiset.remove("two", 2);

// Multimap是一个key对应多个value的map
// 创建一个multimap
Multimap<String, Object> multimap = HashMultimap.create();

// 可以在相同的key下put值,put后map元素为{a=[1, 2]}
multimap.put("a", 1);
multimap.put("a", 2);

// 可以通过putAll为一个key一次性放置多个value,value不保证有序
multimap.putAll("b", Sets.newHashSet(3, 4, 5));

1.3、不可变集合

可以创建不可变集合,这样可以保证集合中的元素不发生变化。如果使用的jdk是9以上版本,也可以直接使用jdk自带的api。

// 创建不可变list,上面为guava api,下面为jdk9 api
List<Integer> immutableList = ImmutableList.of(1, 2, 3);
List<Integer> jdkImmutableList = List.of(1, 2, 3);

// 创建不可变set,上面为guava api,下面为jdk9 api
Set<Integer> immutableSet = ImmutableSet.of(1, 2, 3);
Set<Integer> jdkImmutableSet = Set.of(1, 2, 3);

// 创建不可变map,上面为guava api,下面为jdk9 api
Map<String, Integer> immutableMap = ImmutableMap.of("a", 1, "b", 2, "c", 3);
Map<String, Integer> jdkImmutableMap = Map.of("a", 1, "b", 2, "c", 3);

1.4、集合排序

jdk8之后,Ordering类已废弃,建议使用jdk8的Stream和Comparator。

1.5、流式处理

jdk8之后,FluentIterable类已废弃,建议使用jdk8的Stream。

2、图结构处理

图是由边和顶点组成的数据结构,边将各个顶点进行联结。guava的graph包就是为了模型化图这种数据结构而创建的。

graph包中提供了三种主要的graph类型,即Graph、ValueGraph和Network。

Graph是最基本的图,它的边仅仅是为了联结顶点,本身没有标识或属性。

ValueGraph的边有自己的值(或者说权重),当你需要关注边的信息时可以使用该类型。

Network指一对节点间不止一个边联结,并且这些边可以通过唯一标识进行区分。

下面给出一些常用的方法:

// 创建一个可变的无向图
MutableGraph<Integer> graph = GraphBuilder.undirected().build();

// 创建一个可变的有向值图
MutableValueGraph<City, Distance> roads = ValueGraphBuilder.directed()
        .incidentEdgeOrder(ElementOrder.stable())
        .build();

// 创建一个可变的有向网络
MutableNetwork<Webpage, Link> webSnapshot = NetworkBuilder.directed()
        .allowsParallelEdges(true)
        .nodeOrder(ElementOrder.natural())
        .expectedNodeCount(100000)
        .expectedEdgeCount(1000000)
        .build();

// 创建一个不可变的图
ImmutableGraph<Country> countryAdjacencyGraph =
        GraphBuilder.undirected()
                .<Country>immutable()
                .putEdge(FRANCE, GERMANY)
                .putEdge(FRANCE, BELGIUM)
                .putEdge(GERMANY, BELGIUM)
                .addNode(ICELAND)
                .build();

// 有向图/无向图添加边
directedGraph.addEdge(nodeU, nodeV, edgeUV_a);
directedGraph.addEdge(nodeU, nodeV, edgeUV_b);
directedGraph.addEdge(nodeV, nodeU, edgeVU);

undirectedGraph.addEdge(nodeU, nodeV, edgeUV_a);
undirectedGraph.addEdge(nodeU, nodeV, edgeUV_b);
undirectedGraph.addEdge(nodeV, nodeU, edgeVU);

3、并发工具

3.1、ListenableFuture

ListenableFuture是jdk的Future接口的一个扩展,guava团队建议在所有使用Future的地方,使用ListenableFuture进行代替。

ListenableFuture增加了Future执行成功或失败的监听器,可以在计算完成时即时进行回调,使得并发编程的代码更加高效。

// 使用listeningDecorator方法包装线程池
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));

// 调用submit方法,即可返回ListenableFuture
ListenableFuture<Explosion> explosion = service.submit(
        new Callable<Explosion>() {
            public Explosion call() {
                return pushBigRedButton();
            }
        });

// 通过Futures工具类的addCallback方法,为ListenableFuture添加执行成功和失败的回调
Futures.addCallback(
        explosion,
        new FutureCallback<Explosion>() {

            // 执行成功
            public void onSuccess(Explosion explosion) {
                walkAwayFrom(explosion);
            }

            // 执行失败
            public void onFailure(Throwable thrown) {
                battleArchNemesis();
            }
        },
        service);

3.2、Service

Service接口代表了对象的运行状态,它提供了开始和停止的方法。计时器、RPC服务器等服务均可以实现此接口,以便管理开启和结束等状态。

Service提供了五个正常的状态和一个失败的状态,即:

  • Service.State.NEW 新建
  • Service.State.STARTING 开启
  • Service.State.RUNNING 运行中
  • Service.State.STOPPING 正在停止
  • Service.State.TERMINATED 中止
  • Service.State.FAILED 失败

正常服务的生命周期应该为:

新建->开启->运行中->停止->中止

4、I/O

封装了一些更方便的I/O工具。

// 将整个输入流读为byte数组,不会关闭流
byte[] bytes = ByteStreams.toByteArray(inputStream);

// 将整个输入流中的字节复制到输出流中,不会关闭流或清空缓存区
long length = ByteStreams.copy(inputStream, outputStream);

// 将整个输入流/可读对象中的数据按行读出,不包含换行符
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream));

// 将整个输入流/可读对象中的数据读成一个字符串,不关闭流
String s = CharStreams.toString(new InputStreamReader(inputStream));

// 读取一个文本文件所有行
ImmutableList<String> textLines = Files.asCharSource(file, Charsets.UTF_8)
        .readLines();

// 计算一个文件里出现的不同单词的次数
Multiset<String> wordOccurrences = HashMultiset.create(
        Splitter.on(CharMatcher.whitespace())
                .trimResults()
                .omitEmptyStrings()
                .split(Files.asCharSource(file, Charsets.UTF_8).read()));

5、哈希

提供更复杂的哈希算法,包括Bloom filter数据结构,该结构通过允许少量的错误来节省大量的存储空间。

// 要计算hashCode的实体类
class Person {
    int id;
    String firstName;
    String lastName;
    int birthYear;
}

// 将对象分解为基础属性值的数据漏斗
Funnel<Person> personFunnel = (person, into) -> into
        .putInt(person.id)
        .putString(person.firstName, Charsets.UTF_8)
        .putString(person.lastName, Charsets.UTF_8)
        .putInt(person.birthYear);

// 初始化哈希函数
HashFunction hf = Hashing.sha256();

// 计算哈希值
HashCode hc = hf.newHasher()
        .putLong(id)
        .putString(name, Charsets.UTF_8)
        .putObject(person, personFunnel)
        .hash();

// 创建BloomFilter
BloomFilter<Person> friends = BloomFilter.create(personFunnel, 500, 0.01);

// 向Bloom Filter对象中添加元素
for (Person friend : friendsList) {
    friends.put(friend);
}

// 判断Bloom Filter里是否可能包含某个元素,这里牺牲一定的准确度来换取空间和执行效率
if (friends.mightContain(dude)) {
    // 如果不包含元素并且代码执行到这里的概率是1%。
}

6、缓存

本地缓存(内存级别的缓存),支持各种方式的过期行为,不会将数据存到硬盘。

// 创建缓存
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<>() {
            @Override
            public Graph load(Key key) throws Exception {
                // 通过key来生成值,这个过程消耗资源比较大,因此需要缓存
                return createExpensiveGraph(key);
            }
        });

// 读取缓存中的值
try {
    // 尝试获取缓存中的值,如果没有值,通过CacheLoader执行load方法添加值
    graphs.get(key);
} catch (ExecutionException e) {
    // 异常处理
}

7、事件总线

使组件间保持“发布-订阅”风格的通信,不需要将组件互相注册。它是为了替换使用显式注册发布事件的方式,而非通用的“发布-订阅”系统。

// 事件处理器
class EventBusChangeRecorder {
    @Subscribe
    public void recordCustomerChange(ChangeEvent e) {
        recordChange(e.getChange());
    }
}

// 订阅事件
EventBus eventBus = new EventBus("event");
eventBus.register(new EventBusChangeRecorder());

// 创建事件
ChangeEvent e = getChangeEvent();

// 发布事件
eventBus.post(e);

8、基础工具

8.1、空值处理

jdk8之后,Optional类已废弃,建议使用jdk8同名的Optional。

8.2、前置条件

可以检查参数是否合法、状态是否正确、是否为空值。

// 参数检查,检查不通过抛出IllegalArgumentException
Preconditions.checkArgument(age >= 18 && age <= 70, "年龄应该在%s和%s之间", 18, 70);

// 状态检查,检查不通过抛出IllegalStatementException
Preconditions.checkState(state == OK, "当前状态不正确,应该为%s", "OK");

// 空值检查,检查通过返回原值,不通过抛出NullPointerException
nonNull = Preconditions.checkNotNull(nonNull, "当前值不可以为空");

8.3、反射工具

提供更方便的反射工具。

// 获取类的包名
final String packageName = Reflection.getPackageName(Test.class);

// 生成一个接口的动态代理对象
Foo foo = Reflection.newProxy(Foo.class, invocationHandler);

8.4、数学库

提供了一些JDK没有提供的数学工具,这些工具已经经过了彻底的测试。数学库中的函数很多,下面是一些简单的例子:

// 保证相加的结果不溢出,如果溢出则抛出ArithmeticException
int sum = IntMath.checkedAdd(a, b);

// 获取两个整数的最大公约数
int gcd = IntMath.gcd(a, b);

// 比较两个浮点数是否在容差范围内相等
final boolean equals = DoubleMath.fuzzyEquals(1.0 / 3, 0.3333333, 0.00001);

// 保证幂运算的结果不溢出,如果溢出则抛出ArithmeticException
final long checkedPow = LongMath.checkedPow(5, 20);

// 将BigDecimal数字近似为double,如果数字过大,则返回Double.MAX_VALUE。
final double v = BigDecimalMath.roundToDouble(BigDecimal.valueOf(2).pow(2000), HALF_UP);

9、依赖引入

guava库区分jre版本(用于8.0及以上版本JDK)和android版本(用于android平台和7.0以下版本的JDK)。在项目中可以通过maven引入guava库,方式如下:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
    <!-- 如果是Android版本,则用下面的version标签: -->
    <version>30.1.1-android</version>
</dependency>

10、注意事项

1、部分功能已加入到JDK中,可以优先使用JDK中的相同功能,这些功能包括:

  • 用于空值处理Optional的类
  • 基础的函数式接口,如Function、Predicate、Supplier
  • 流式计算,FluentIterable的操作相当于JDK8中的Stream操作。
  • I/O操作,Files工具类
  • 不可变集合,jdk中有ImmutableCollections工具类

2、所有的类都有自己的使用场景。应该多读使用文档,看这些工具是否符合自己的使用场景。

发表评论