跳至主要內容

Guava 笔记

工具Guava

基本工具

Optional

Guava 的Optional<T>是用来处理可能为 null 值的容器类,在业务代码里,应该明确区分 null 和 空 的含义,避免混淆null/空的语义,提高程序的健壮性。JDK 8 开始也提供了 Optional 提供相同的功能,而且支持函数式编程的特性,因此建议使用 JDK 标准自带的 Optional。

// 1. 创建包含非空值的 Optional
Optional<String> optional = Optional.of("Hello");

// 2. 创建可能为 null 的 Optional
Optional<String> optional = Optional.ofNullable(null);

// 3. 创建空的 Optional
Optional<String> emptyOptional = Optional.empty();

// 4. 判断 Optional 是否包含值并取值,空 Optional 调用 get 会抛出 NoSuchElementException
if(optional.isPresent())
    System.out.println(optional.get());

// 5. 提供默认值
String value = optional.orElse("Default Value");

// 6. 提供默认 Supplier
String value = optional.orElseGet(() -> generateDefaultValue());

// 7. 空 Optional 抛异常
String value = optional.orElseThrow(() -> new IllegalStateException("Value not present"));

// 8. Function 映射,并用 Optional 包装映射结果
Optional<String> transformedOptional = optional.map(value -> value.toUpperCase());

// 9. Function 扁平映射,不会嵌套包装
Optional<String> transformedOptional = optional.flatMap(value -> Optional.of(value.toUpperCase()));

Preconditions

Preconditions提供了若干前置条件判断的使用方法,每个方法都有三种参数重载:

  1. 仅 boolean 校验,抛出异常,没有错误消息
  2. 指定 Object 对象,其 toString() 的结果作为错误消息
  3. 指定 String 格式化串作为错误消息,并且可以附加 Object 作为消息的参数
// 校验参数
checkArgument(boolean)

// 校验空值
checkNotNull(T)

// 校验索引
checkElementIndex(int index, int size)

// ......

JDK 7 开始提供的 Objects 类也提供了一些类似的功能,具体可以参考 JDK Docopen in new window

Objects

// 避免空指针异常
Objects.equal("a", "a");

Objects.hashCode(o1, o2);

MoreObjects.toStringHelper(object).add("key", "val").addValue("End").toString();

// 比较器链,从前往后直到比较到非零的结果结束
ComparisonChain.start()
        .compare(this.aString, that.aString)
        .compare(this.anInt, that.anInt)
        .compare(this.anEnum, that.anEnum, Ordering.natural().nullsLast())
        .result();

Ordering

Guava Fluent 风格的比较器实现,可以构建复杂的逻辑完成集合排序。内置的排序器有以下几个:

ARBITRARY_ORDERING随机排序
NaturalOrdering自然排序
AllEqualOrdering全等排序,包括 null
UsingToStringOrdering按对象的 toString() 做字典排序

当然也可以通过继承 Ording 抽象类,以及各种 API 自定义排序逻辑。

// 对元素执行 Function -> null 元素前置 -> 自然排序 -> 逆序
Ordering<Foo> ordering = Ordering.natural().reverse().nullsFirst().onResultOf(new Function<Foo, String>() {
    public String apply(Foo foo) {
    	return foo.sortedBy;
    }
});

// 获取可迭代对象中,按排序器的逻辑最大的 k 个元素
ordering.greatest(Iterator, k);

// 是否已经按排序器有序
ordering.isOrdered(Iterable);

// 按排序器逻辑最小的元素
ordering.min(a, b, c...);

Throwables

看了一下文档和 API,Throwables 工具类貌似没什么实用的意义,官方文档也在考虑这个类的作用,新版已经废弃了一部分 API,参考:Why we deprecated Throwables.propagateopen in new window

// 如果 t 是 Error/RuntimeException 直接抛出,否则包装成 RuntimeException 抛出
Throwables.propagate(t);

// t 为 aClass 类型才抛出
Throwables.propagateIfInstanceOf(t, aClass);

// t 为 aClass/Error/RuntimeException 才抛出
Throwables.propagateIfPossible(t, aClass);

集合

Immutable

在程序设计中使用不可变对象,可以提高代码的可靠性和可维护性,其优势包括:

  1. 线程安全性(Thread Safety):不可变对象是线程安全的,无需同步操作,避免了竞态条件
  2. 安全性:可以防止在程序运行时被意外修改,提高了程序的安全性
  3. 易于理解和测试:不可变对象在创建后不会发生变化,更容易理解和测试
  4. 克隆和拷贝:不可变对象不需要实现可变对象的复制(Clone)和拷贝(Copy)逻辑,因为它们的状态不可变,克隆即是自己

JDK 的 Collections 提供了 Unmodified Collections 不可变集合,但仅仅是通过装饰器模式提供了一个只读的视图,并没有阻止对原始集合的修改操作,并且效率较低。
而 Guava 提供的不可变集合更加简单高效,确保了真正的不可变性。

// copyOf 会尝试在安全的时候避免做拷贝
ImmutableList<String> immutableList1 = ImmutableList.copyOf(origin);
ImmutableList<String> immutableList2 = ImmutableList.of("A", "B", "C");
ImmutableList<String> immutableList3 = ImmutableList.<String>builder()
                                                .add("A", "B", "C")
                                                .build();

// 任何不可变集合可以转变为 ImmutableList,且该列表视图通常性能很好
ImmutableList<String> list = immutable.asList();

新集合类型

Multiset

Multiset 是一个新的集合类型,可以多次添加相等的元素,既可以看成是无序的列表,也可以看成存储元素和对应数量的键值对映射[E1: cnt1; E2:cnt2]。常用实现包括 HashMultiset, TreeMultiset, LinkedHashMultiset...

Multiset<String> multiset = HashMultiset.create();
multiset.add("A");
multiset.add("A");
multiset.add("B");
// 输出:[A x 2, B]
log.debug("{}", multiset);

// 元素总数
log.debug("{}", multiset.size());
// 不重复元素个数
log.debug("{}", multiset.elementSet().size());
// 设置元素计数
multiset.setCount("A", 3);
// 获取元素个数
log.debug("{}", multiset.count("A"));

Multimap

支持将 key 映射到多个 value 的方式,而不用定义Map<K, List<V>> 或 Map<K, Set<V>>这样的哈皮形式。实现类包括ArrayListMultimap, HashMultimap, LinkedListMultimap, TreeMultimap...

// 列表实现
ListMultimap<String, Integer> listMultimap = MultimapBuilder.hashKeys().arrayListValues().build();
// 集合实现
SetMultimap<String, Integer> setMultimap = MultimapBuilder.treeKeys().hashSetValues().build();

listMultimap.put("A", 1);
listMultimap.put("A", 2);
listMultimap.put("B", 1);
// {A=[1, 2], B=[1, 2]}
log.debug("{}", listMultimap);
// [1, 2],不存在则返回一个空集合
log.debug("{}", listMultimap.get("A"));
// [1, 2] 移除 key 关联的所有 value
List<Integer> valList = listMultimap.removeAll("A");

// 返回普通 map 的视图,仅支持 remove,不能 put,且会更新原始的 listMultimap
Map<String, Collection<Integer>> map = listMultimap.asMap();

BiMap

Map 可以实现 key -> value 的映射,如果想要 value -> key 的映射,就需要定义两个 Map,并且同步更新,很不优雅。Guava 提供了 BiMap 支持支持双向的映射关系,常用实现有HashMap, EnumBiMap, EnumHashBiMap...

BiMap<String, Integer> biMap = HashBiMap.create();
biMap.put("A", 100);

// 删除已存在的 KV,重新添加 KV
biMap.forcePut("A", 200);

// 获取反向映射
BiMap<Integer, String> inverse = biMap.inverse();
log.debug("{}", inverse.get(100));

Table

当需要同时对多个 key 进行索引时,需要定义Map<key1, Map<key2, val>>这样的形式,ugly and awkward。Guava 提供了 Table 用于支持类似 row、column 的双键映射。实现包括HashBasedTable, TreeBasedTable, ArrayTable...

// Table<R, C, V>
Table<String, String, Integer> table = HashBasedTable.create();
table.put("row1", "col1", 1);
table.put("row1", "col2", 2);
table.put("row2", "col1", 3);
table.put("row2", "col2", 4);

// 获取类似 Map.Entry 的 Table.Cell<R, C, V>
table.cellSet().forEach(System.out::println);

// Map<R, Map<C, V>> 视图
Map<String, Map<String, Integer>> rowMap = table.rowMap();
Set<String> rowKeySet = table.rowKeySet();
// Map<C, Map<R, V>> 视图(基于列的索引效率通常比行低)
Map<String, Map<String, Integer>> columnMap = table.columnMap();
Set<String> columnedKeySet = table.columnKeySet();

其它

  • ClassToInstanceMap:Class -> Instance 的映射,可以避免强制类型转换
  • RangeSet (Beta状态):范围集合,自动合并、分解范围区间
RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1, 10]}
rangeSet.add(Range.closedOpen(11, 15)); // disconnected range: {[1, 10], [11, 15)}
rangeSet.add(Range.closedOpen(15, 20)); // connected range; {[1, 10], [11, 20)}
rangeSet.add(Range.openClosed(0, 0)); // empty range; {[1, 10], [11, 20)}
rangeSet.remove(Range.open(5, 10)); // splits [1, 10]; {[1, 5], [10, 10], [11, 20)}

// [[1..5], [10..10], [11..20)]
System.out.println(rangeSet);
  • RangeMap (Beta状态):range -> value 的映射,不会自动合并区间
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"}

// [[1..3]=foo, (3..5)=bar, (11..20)=foo]
System.out.println(rangeMap);

集合工具

JDK 自带的java.util.Collections提供了很多实用的功能,而 Guava 针对特定接口提供了更多工具,例如:

  • 提供很多静态工厂方法、包装器
  • Iterables 支持懒加载的集合视图操作
  • Sets 提供集合论运算
  • Maps 提供 diff 计算
  • Multimaps 提供 Map -> Multimap 的转换,并支持反向映射
  • Tables 提供行列转置 | Interface | JDK or Guava | Guava Utility Class | | --- | --- | --- | | Collection | JDK | Collections2 | | List | JDK | Lists | | Set | JDK | Sets | | Map | JDK | Maps | | Queue | JDK | Queues | | Multiset | Guava | Multisets | | Multimap | Guava | Multimaps | | BiMap | Guava | Maps | | Table | Guava | Tables |

另外,还可以继承 Forwarding 通过装饰器模式装饰特殊实现,PeekingIterator 可以 peek 下一次返回的元素,AbstractIterator 自定义迭代方式等等。(PS:这些东西也许有点用,可是能用吗,业务里写这种代码怕是要被人喷死...)

缓存✨

Guava 提供的 Cache 按照 **有则取值,无则计算 **的逻辑,支持自动装载,自动移除等扩展功能,比传统的ConcurrentHashMap功能更加强大。

使用

除了继承 AbstractCache 自定义缓存实现外,通常可以直接使用 CacheBuilder 构建一个 LocalLoadingCache 缓存,通过 key 来懒加载相关联的 value。如果 key-value 没有关联关系可以使用无参的 build 方法返回一个LocalManualCache对象,等调用 get 时再传递一个 callable 对象获取 value。

LoadingCache<String, Integer> loadingCache = CacheBuilder.newBuilder()
                // 最大容量
				.maximumSize(1000)
            	// 定时刷新
				.refreshAfterWrite(10, TimeUnit.MINUTES)
                // 添加移除时的监听器
                .removalListener(notification -> System.out.println(
                    notification.getKey() + " -> " + 
                    notification.getValue() + " is removed by " + 
                    notification.getCause()))
            	// 并发等级
				.concurrencyLevel(4)
				// 开启统计功能
            	.recordStats()
                .build(
                        new CacheLoader<String, Integer>() {
                            @Override
                            public Integer load(String key) {
                                System.out.println("Loading key: " + key);
                                return Integer.parseInt(key);
                            }
                        });

loadingCache.get("key");
// 使失效
loadingCache.invalidate("key");
// get with callable
loadingCache.get("key", () -> 2));
// 刷新缓存
loadingCache.refresh("key");
// 返回一个 map 视图
ConcurrentMap<String, Integer> map = loadingCache.asMap();

LocalLoadingCache继承自 LocalManualCache,里面封装了一个继承自 Map 的localCache成员存储实际的 KV 并通过分段锁实现线程安全,另外实现了 Cache 接口定义了一系列缓存操作。

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>

失效

Guava 提供了三种基础的失效策略:

  • 基于容量失效:
    • maximumSize(long) 指定最大容量
    • weigher(Weigher)实现自定义权重,配合maximumWeight(long)以权重作为容量
  • 基于时间失效:在写入、偶尔读取期间执行定期维护
    • expireAfterAccess(long, timeUnit) 读写后超出指定时间即失效
    • expireAfterWrite(long, timeUnit) 写后超出指定时间即失效
  • 基于引用失效:
    • weakKeys()通过弱引用存储 key
    • weakValues()通过弱引用存储 value
    • softValues() 通过软引用包装 value,以 LRU 方式进行 GC

另外,也可以通过invalidate手动清除缓存。缓存不会自动进行清理,Guava 会在写操作期间,或者偶尔在读操作时进行过期失效的维护工作。缓存刷新操作的时机也是类似的。

com.google.common.graph提供了多种图的实现:

  • Graph 边是匿名的,且不关联任何信息
  • ValueGraph 边拥有自己的值,如权重、标签
  • **Network **边对象唯一,且期望实施对其引用的查询

整体看下来,感觉还是挺复杂的,不太常用。等有需要再学习吧...

并发

ListenableFuture

JUC 的 Future 接口提供了一种异步获取任务执行结果的机制,表示一个异步计算的结果。

ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(() -> {
    // 执行异步任务,返回一个结果
    return "Task completed";
});
// Blocked
String result = future.get();

Executor 实际返回的是实现类 FutureTask,它同时实现了 Runnable 接口,因此可以手动创建异步任务。

FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return "Hello";
    }
});
        
new Thread(futureTask).start();
System.out.println(futureTask.get());

而 Guava 提供的 ListenableFuture 更进一步,允许注册回调,在任务完成后自动执行,实际也是使用它的实现类 ListenableFutureTask。

// 装饰原始的线程池
ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
ListenableFuture<String> future = listeningExecutorService.submit(() -> {
    // int i = 1 / 0;
    return "Hello";
});

// 添加回调 1
Futures.addCallback(future, new FutureCallback<String>() {
    // 任务成功时的回调
    @Override
    public void onSuccess(String result) {
        System.out.println(result);
    }

    // 任务失败时的回调
    @Override
    public void onFailure(Throwable t) {
        System.out.println("Error: " + t.getMessage());
    }
}, listeningExecutorService);

// 添加回调 2
future.addListener(new Runnable() {
    @Override
    public void run() {
        System.out.println("Done");
    }
}, listeningExecutorService);

Service

Guava 的 Service 框架是一个用于管理服务生命周期的轻量级框架。它提供了一个抽象类 AbstractService 和一个接口 Service,可以通过继承 AbstractService 或者直接实现 Service 接口来创建自定义的服务,并使用 ServiceManager 来管理这些服务的生命周期。

public class MyService extends AbstractService {
    @Override
    protected void doStart() {
        // 在这里执行启动服务的逻辑
        System.out.println("MyService is starting...");
        notifyStarted();
    }
    
    @Override
    protected void doStop() {
        // 在这里执行停止服务的逻辑
        System.out.println("MyService is stopping...");
        notifyStopped();
    }
}


@Test
public void testService() {
    Service service = new MyService();
    ServiceManager serviceManager = new ServiceManager(List.of(service));
    
    serviceManager.startAsync().awaitHealthy();
    
    // 主线程逻辑
    
    serviceManager.stopAsync().awaitStopped();
}

Strings

Guava 提供了一系列用于字符串处理的工具:

  1. Joiner:字符串拼接工具,创建的都是不可变实例
Joiner joiner = Joiner.on(";").useForNull("^");
// "A;B;^;D"
String joined = joiner.join("A", "B", null, "D");
  1. Splitter: 字符串分割工具,创建的也是不可变实例
// String#split 反直觉的输出:["", "a", "", "b"]
Arrays.stream(",a,,b,".split(",")).toList().forEach(System.out::println);
// ["foo", "bar", "qux"]
Iterable<String> split = Splitter.on(",")
    	// 结果自动 trim
        .trimResults()
    	// 忽略结果中的空串
        .omitEmptyStrings()
        // 限制分割数
        .limit(3)
        .split("foo,bar,,   qux");
Map<String, String> splitMap = Splitter.on(";")
	// 指定 K-V 的分隔符可以将键值对的串解析为 Map
	.withKeyValueSeparator("->")
	.split("A->1;B->2");
  1. CharMatchers:字符序列匹配和处理的工具,内置了大量常用的匹配器。使用上通常分两步:
    • 确定匹配的字符和模式
    • 用匹配的字符做处理
// 确定匹配的字符和模式,例如 anyOf, none, whitespace, digit, javaLetter, javaIsoControl...
CharMatcher matcher = CharMatcher.anyOf("abc");
// defg
log.debug("{}", matcher.removeFrom("abcdefg"));
// abc
log.debug("{}", matcher.retainFrom("abcdefg"));
// true
log.debug("{}", matcher.matchesAllOf("abc"));
// hhh 
log.debug("{}", matcher.trimFrom("abchhhabc"));
// ___hhh___
log.debug("{}", matcher.replaceFrom("abc hhh abc", "_"));
  1. Charsets: 提供了6种标准的字符集常量引用,例如Charsets.UTF_8。JDK 7 以后建议使用内置的 StandardCharsets
  2. CaseFormat: 大小写转换的工具
// UPPER_UNDERSCORE -> LOWER_CAMEL
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "CONSTANT_NAME"));
FormatExample
LOWER_CAMELopen in new windowlowerCamel
LOWER_HYPHENopen in new windowlower-hyphen
LOWER_UNDERSCOREopen in new windowlower_underscore
UPPER_CAMELopen in new windowUpperCamel
UPPER_UNDERSCOREopen in new windowUPPER_UNDERSCORE
  1. Strings:也提供了几个没什么大用的小工具

基本类型工具

Java 的基本类型包括8个:byte、short、int、long、float、double、char、boolean。Guava 提供了若干工具以支持基本类型和集合 API 的交互、字节数组转换、无符号形式的支持等等。

基本类型Guava 工具类
byteBytes, SignedBytes, UnsignedBytes
shortShorts
intInts, UnsignedInteger, UnsignedInts
longLongs, UnsignedLong, UnsignedLongs
floatFloats
doubleDoubles
charChars
booleanBooleans

Range

Guava 提供了 Range 类以支持范围类型,并且支持范围的运算,比如包含、交集、并集、查询等等。

Range.open(a, b);			// (a, b)
Range.closed(a, b);			// [a..b]
Range.closedOpen(a, b);		// [a..b)
Range.openClosed(a, b);		// (a..b]
Range.greaterThan(a);		// (a..+∞)
Range.atLeast(a);			// [a..+∞)
Range.lessThan(a);			// (-∞..b)
Range.atMost(a);			// (-∞..b]
Range.all();				// (-∞..+∞)

// 通用创建方式
Range.range(a, BoundType.CLOSED, b, BoundType.OPEN);

IO

Guava 使用术语 **流 **来表示可关闭的,并且在底层资源中有位置状态的 I/O 数据流。字节流对应的工具类为 ByteSterams,字符流对应的工具类为 CharStreams。
Guava 中为了避免和流直接打交道,抽象出可读的 源 source 和可写的 汇 sink 两个概念,指可以从中打开流的资源,比如 File、URL,同样也分别有字节和字符对应的源和汇,定义了一系列读写的方法。
除此之外,Files 工具类提供了对文件的操作。(PS:个人觉得,JDK 的 IO 流已经够麻烦的了,又来一套 API 太乱了,而且也没有更好用吧。还是先统一用 JDK 标准库里的好一点。)

Hash

JDK 内置的哈希限定为 32 位的 int 类型,虽然速度很快但质量一般,容易产生碰撞。为此 Guava 提供了自己的 Hash 包。

  • Hashing 类内置了一系列的散列函数对象 HashFunction,包括 murmur3, sha256, adler32, crc32等等。
  • 确定 HashFunction 后进而拿到继承自 PrimitiveSink 的 Hasher 对象。
  • 作为一个汇,可以往 Hasher 里输入数据,可以是内置类型,也可以是自定义类型,但需要传递一个 Funnel 定义对象分解的方式。
  • 最后计算得到 HashCode 对象。
HashFunction hf = Hashing.adler32();

User user = new User("chanper", 24);
HashCode hash = hf.newHasher()
        .putLong(20L)
        .putString("chanper", StandardCharsets.UTF_8)
    	// 输入自定义类,同时需要一个 Funnel
		.putObject(user, userFunnel)
        .hash();

// Funnel 定义对象分解的方式
Funnel<User> userFunnel = new Funnel<>() {
    @Override
    public void funnel(User user, PrimitiveSink into) {
        into
            .putString(user.name(), StandardCharsets.UTF_8)
            .putInt(user.age());
    }
};

另外,Guava 库也内置了一个使用简便布隆过滤器。

// funnel 对象,预期的插入数量,false positive probability
BloomFilter<User> friends = BloomFilter.create(userFunnel, 500, 0.01);
for (int i = 0; i < 1000; i++) {
    friends.put(new User("user_" + i, 24));
}

if(friends.mightContain(somebody)) {
    System.out.println("somebody is in friends");
}

EventBus

EventBus 是 Guava 提供的一个事件总线库,用于简化组件之间的通信。通过 EventBus,你可以实现发布/订阅模式,组件之间可以松散地耦合,使得事件的发布者(Producer)和订阅者(Subscriber)之间不需要直接依赖彼此。 使用时注意:

  • 一个订阅者可以处理多个不同的事件,取决于处理方法的参数,并且支持泛型的通配符
  • 没有对应的监听者则会把事件封装为DeadEvent,可以定义对应的监听器
// 事件类型
public record MessageEvent(String message) {}

public class MessageSubscriber {
    // 事件处理方法标记
    @Subscribe
    public void handleMessageEvent(MessageEvent event) {
        System.out.println("Received message: " + event.message());
    }

    // 一个订阅者可以处理多个事件
    @Subscribe
    public void handleMessageEvent(MessageEvent2 event2) {
        System.out.println("Received message2: " + event2.message());
    }
}

@Test
public void testEvnetBus() {
    // 创建EventBus实例
    EventBus eventBus = new EventBus();
    
    // 注册订阅者
    MessageSubscriber subscriber = new MessageSubscriber();
    eventBus.register(subscriber);
    
    // 发布事件
    MessageEvent event = new MessageEvent("Hello, EventBus!");
    eventBus.post(event);
}

参考

  1. https://guava.dev/open in new window
  2. https://github.com/google/guavaopen in new window
  3. Google Guava官方教程(中文版) | Google Guava 中文教程open in new window (旧版)
  4. (Guava 译文系列)Guava 用户手册open in new window (新版)