Guava:缓存、引用类型与集合工具

欢迎你来读这篇博客,这篇博客主要是关于Guava 缓存、引用类型与集合工具
其中包括 LRU 算法、强引用、软引用、弱引用、虚引用、ReferenceQueue、手写 InMemoryCache、Guava Cache、CacheBuilder、CacheLoader、LoadingCache、驱逐策略、时间过期、RemovalListener、refresh、recordStats、CacheBuilderSpec,以及 FluentIterable、Lists、Sets、Maps、BiMap、Multimap、Table、Range、RangeSet、RangeMap、Immutable Collections、Sorted Collections 等集合工具。

序言

Guava 里最有工程价值的模块之一,就是缓存和集合工具。

如果前面几章更多是“工具类入门”,那么这一章就是 Guava 中非常贴近真实后端业务的一章。

因为后端系统里经常会遇到这些问题:

  • 热点数据如何缓存;
  • 缓存满了如何淘汰;
  • 如何实现 LRU;
  • 缓存中的对象会不会导致内存泄漏;
  • 软引用、弱引用到底有什么区别;
  • Guava Cache 如何自动加载数据;
  • 缓存过期是按写入时间还是访问时间;
  • 缓存驱逐后如何收到通知;
  • 缓存命中率如何统计;
  • 一对多映射怎么优雅表达;
  • 双向映射怎么实现;
  • 二维表结构怎么表示;
  • 区间匹配怎么做;
  • 不可变集合有什么优势;
  • Java Stream 出现后,Guava Collections 还有哪些价值。

这一章内容很多,可以分成三个大模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第 9 章:缓存、引用类型与集合工具

一、LRU 与引用类型
9.1 LRU 算法原理以及两种 LRU 算法的实现
9.2 SoftReference、WeakReference、PhantomReference 精讲
9.3 SoftReference 加 LRU 算法实现 InMemoryCache

二、Guava Cache
9.4 CacheLoader、CacheBuilder、LoadingCache 以及两种驱逐策略
9.5 WeakKey、SoftValues、时间逐出的两种策略
9.6 NullValue、Removal 通知、Refresh、预加载
9.7 RecordStats、CacheBuilderSpec 详解

三、Guava Collections
9.8 FluentIterable
9.9 Lists
9.10 Sets
9.11 Maps、BiMap、Multimap
9.12 Table、Range
9.13 Range、RangeMap
9.14 Immutable Collections、Sorted Collections

这篇文章不是 API 罗列,而是按“为什么需要它、怎么用、什么时候不用、和 JDK/Java Stream 怎么取舍”的方式讲。

特别注意:

Guava Cache 是非常优秀的本地缓存工具,但在现代新项目中,如果你追求更强的缓存性能和更多现代特性,也经常会看到 Caffeine。本文重点仍然围绕 Guava 课程体系展开。

正文

chapter 1:缓存到底解决什么问题

缓存的核心目的是:

用空间换时间,把高频读取、计算成本高、变化不频繁的数据暂存在更快的位置。

常见缓存场景:

  • 用户信息缓存;
  • 商品详情缓存;
  • 权限信息缓存;
  • 字典配置缓存;
  • 热点数据缓存;
  • 第三方接口结果缓存;
  • 复杂计算结果缓存;
  • 规则引擎规则缓存;
  • 文件元数据缓存。

缓存能提升性能,但也会带来复杂度:

  • 缓存何时过期;
  • 缓存何时更新;
  • 缓存满了怎么淘汰;
  • 缓存穿透怎么办;
  • 缓存击穿怎么办;
  • 缓存雪崩怎么办;
  • 缓存是否允许 null;
  • 本地缓存和分布式缓存如何配合;
  • 缓存数据和数据库是否一致。

Guava Cache 主要解决的是:

1
单 JVM 内的本地缓存问题。

它不是 Redis,不是分布式缓存。

它适合:

  • 单机热点数据;
  • 本地字典;
  • 本地规则;
  • 短生命周期计算结果;
  • 避免重复加载;
  • 降低远程调用频率。

chapter 2:LRU 算法基本思想

LRU 是 Least Recently Used 的缩写。

意思是:

最近最少使用。

它的核心思想是:

1
如果缓存满了,优先淘汰最久没有被访问的数据。

因为一个常见假设是:

最近被访问过的数据,接下来更可能再次被访问。

例如缓存容量是 3。

依次访问:

1
A B C

缓存:

1
A B C

再次访问 A:

1
B C A

此时 A 是最近访问的,B 是最久未访问的。

再访问 D,缓存满了,要淘汰 B:

1
C A D

LRU 需要支持两个核心操作:

1
2
get(key):访问数据,并把它移动到最近使用位置。
put(key, value):写入数据,如果超过容量,淘汰最久未使用的数据。

理想时间复杂度:

1
2
get: O(1)
put: O(1)

要做到 O(1),通常需要:

1
HashMap + 双向链表

chapter 3:LinkedHashMap 实现 LRU

Java 的 LinkedHashMap 已经内置了访问顺序能力。

构造方法:

1
new LinkedHashMap<>(initialCapacity, loadFactor, accessOrder)

accessOrder = true 时,访问元素会调整顺序。

再重写:

1
removeEldestEntry

就可以实现 LRU。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapLruCache<K, V> extends LinkedHashMap<K, V> {

private final int capacity;

public LinkedHashMapLruCache(int capacity) {
super(capacity, 0.75f, true);

if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than 0");
}

this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LinkedHashMapLruDemo {

public static void main(String[] args) {
LinkedHashMapLruCache<String, String> cache = new LinkedHashMapLruCache<>(3);

cache.put("A", "value-A");
cache.put("B", "value-B");
cache.put("C", "value-C");

cache.get("A");

cache.put("D", "value-D");

System.out.println(cache.keySet());
}
}

输出类似:

1
[C, A, D]

B 被淘汰了。

chapter 4:LinkedHashMap 实现 LRU 的优缺点

优点:

  • JDK 原生;
  • 代码简单;
  • 容易理解;
  • 适合单线程或外部加锁场景;
  • 实现 LRU 很方便。

缺点:

  • 默认不是线程安全;
  • 缓存能力简单;
  • 不支持过期时间;
  • 不支持权重;
  • 不支持加载回源;
  • 不支持统计;
  • 不支持 RemovalListener;
  • 不适合复杂缓存场景。

如果只是写一个小工具,LinkedHashMap 足够。

如果要做业务缓存,Guava Cache 更合适。

如果要做高性能现代本地缓存,可以考虑 Caffeine。

chapter 5:HashMap + 双向链表实现 LRU

手写 LRU 的经典结构:

1
HashMap<K, Node<K,V>> + 双向链表

HashMap 用于 O(1) 定位节点。

双向链表用于维护访问顺序。

链表头表示最近使用。

链表尾表示最久未使用。

结构:

1
head <-> node1 <-> node2 <-> node3 <-> tail

访问某个节点时,把它移动到头部。

缓存满时,删除尾部节点。

chapter 6:手写 LRU 节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LruNode<K, V> {

K key;

V value;

LruNode<K, V> prev;

LruNode<K, V> next;

public LruNode(K key, V value) {
this.key = key;
this.value = value;
}
}

chapter 7:手写 LRU Cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import java.util.HashMap;
import java.util.Map;

public class ManualLruCache<K, V> {

private final int capacity;

private final Map<K, LruNode<K, V>> map = new HashMap<>();

private final LruNode<K, V> head = new LruNode<>(null, null);

private final LruNode<K, V> tail = new LruNode<>(null, null);

public ManualLruCache(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than 0");
}

this.capacity = capacity;

head.next = tail;
tail.prev = head;
}

public V get(K key) {
LruNode<K, V> node = map.get(key);

if (node == null) {
return null;
}

moveToHead(node);

return node.value;
}

public void put(K key, V value) {
LruNode<K, V> node = map.get(key);

if (node != null) {
node.value = value;
moveToHead(node);
return;
}

LruNode<K, V> newNode = new LruNode<>(key, value);

map.put(key, newNode);
addToHead(newNode);

if (map.size() > capacity) {
LruNode<K, V> removed = removeTail();

map.remove(removed.key);
}
}

private void moveToHead(LruNode<K, V> node) {
removeNode(node);
addToHead(node);
}

private void addToHead(LruNode<K, V> node) {
node.prev = head;
node.next = head.next;

head.next.prev = node;
head.next = node;
}

private void removeNode(LruNode<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}

private LruNode<K, V> removeTail() {
LruNode<K, V> node = tail.prev;

removeNode(node);

return node;
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ManualLruCacheDemo {

public static void main(String[] args) {
ManualLruCache<String, String> cache = new ManualLruCache<>(2);

cache.put("A", "1");
cache.put("B", "2");

cache.get("A");

cache.put("C", "3");

System.out.println(cache.get("A"));
System.out.println(cache.get("B"));
System.out.println(cache.get("C"));
}
}

输出:

1
2
3
1
null
3

B 被淘汰。

chapter 8:LRU 时间复杂度分析

使用 HashMap + 双向链表 后:

操作 时间复杂度
get O(1)
put 已存在 key O(1)
put 新 key O(1)
删除尾节点 O(1)
移动节点到头部 O(1)

空间复杂度:

1
O(capacity)

因为最多保存 capacity 个节点。

这也是 LRU 常见实现方式。

chapter 9:缓存淘汰策略设计

LRU 只是淘汰策略的一种。

常见策略包括:

策略 含义
FIFO 先进先出
LRU 最近最少使用
LFU 最不经常使用
TTL 按时间过期
Weight 按权重限制容量
Random 随机淘汰
Manual 手动删除

真实缓存系统通常会组合多个策略。

例如 Guava Cache 支持:

1
2
3
4
5
6
7
maximumSize
maximumWeight
expireAfterWrite
expireAfterAccess
weakKeys
weakValues
softValues

这比简单 LRU 更工程化。

chapter 10:Java 引用类型总览

Java 中引用类型可以分为:

1
2
3
4
强引用
软引用 SoftReference
弱引用 WeakReference
虚引用 PhantomReference

它们和 GC 的关系不同。

引用类型 GC 行为 典型场景
强引用 只要强引用存在,对象不会被回收 普通对象
软引用 内存不足时可能被回收 内存敏感缓存
弱引用 只要发生 GC,弱引用对象可能被回收 WeakHashMap、元数据关联
虚引用 不影响生命周期,只用于回收通知 堆外资源清理、对象回收跟踪

理解这些引用类型,是理解缓存和内存管理的重要基础。

chapter 11:强引用

普通对象引用就是强引用。

1
User user = new User();

只要 user 还指向这个对象,GC 就不会回收它。

强引用是最常见的引用方式。

缓存中如果全部使用强引用,就会导致:

1
缓存对象一直活着,直到从缓存中删除。

这适合明确可控的缓存。

例如:

  • maximumSize 控制容量;
  • expireAfterWrite 控制过期;
  • 手动 invalidate。

chapter 12:SoftReference 软引用

软引用:

1
SoftReference<User> reference = new SoftReference<>(new User());

获取对象:

1
User user = reference.get();

软引用对象在内存充足时通常不会被回收。

但在内存紧张时,GC 可能回收它。

软引用常被用于“内存敏感缓存”。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.ref.SoftReference;

public class SoftReferenceDemo {

public static void main(String[] args) {
SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024]);

byte[] data = reference.get();

System.out.println(data != null);
}
}

软引用不是稳定缓存。

它只是告诉 JVM:

1
如果内存紧张,可以回收这个对象。

chapter 13:WeakReference 弱引用

弱引用:

1
WeakReference<User> reference = new WeakReference<>(new User());

弱引用对象只要没有强引用,下一次 GC 就可能被回收。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.ref.WeakReference;

public class WeakReferenceDemo {

public static void main(String[] args) {
WeakReference<Object> reference = new WeakReference<>(new Object());

System.out.println(reference.get() != null);

System.gc();

System.out.println(reference.get() != null);
}
}

第二次可能输出 false。

弱引用常见场景:

  • WeakHashMap;
  • 缓存 key 不应该阻止对象回收;
  • 框架内部元数据关联;
  • 监听器弱引用,避免内存泄漏。

chapter 14:PhantomReference 虚引用

虚引用最特殊。

它的 get() 永远返回 null。

1
PhantomReference<Object> reference = new PhantomReference<>(object, referenceQueue);

虚引用必须配合 ReferenceQueue 使用。

它用于:

在对象被回收时收到通知。

常见场景:

  • 堆外内存清理;
  • 资源释放跟踪;
  • 对象生命周期监控;
  • 更底层的清理机制。

业务缓存中很少直接使用虚引用。

它更多用于框架和底层资源管理。

chapter 15:ReferenceQueue

ReferenceQueue 用于接收被 GC 回收的引用对象。

例如软引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

public class ReferenceQueueDemo {

public static void main(String[] args) {
ReferenceQueue<Object> queue = new ReferenceQueue<>();

Object value = new Object();

SoftReference<Object> reference = new SoftReference<>(value, queue);

value = null;

System.gc();

Object polled = queue.poll();

System.out.println(polled);
}
}

当引用对象被回收后,引用本身可能进入队列。

缓存可以通过 ReferenceQueue 清理已经失效的引用记录。

chapter 16:缓存场景中的引用选择

1. 强引用缓存

优点:

  • 稳定;
  • 可控;
  • 只要不淘汰就存在。

缺点:

  • 容易占内存;
  • 必须设计淘汰策略。

适合:

1
Guava Cache maximumSize + expireAfterWrite

2. 软引用缓存

优点:

  • 内存紧张时可自动释放。

缺点:

  • 回收时机不可控;
  • 命中率不稳定;
  • 现代 JVM 下不建议作为核心缓存策略。

适合:

1
非关键内存敏感缓存

3. 弱引用缓存

优点:

  • 不阻止 key/value 被 GC 回收。

缺点:

  • 数据随 GC 消失;
  • 不适合稳定缓存。

适合:

1
对象元信息关联、弱 key 映射

4. 虚引用

不适合普通缓存。

适合底层资源清理。

chapter 17:SoftReference + LRU 实现 InMemoryCache

我们设计一个简单内存缓存:

需求:

  1. 使用 LRU 控制最大条目数;
  2. value 使用 SoftReference;
  3. 使用 ReferenceQueue 清理被 GC 回收的 value;
  4. 支持 get、put、remove、size。

注意:

这是学习用实现,不建议直接用于生产核心缓存。

生产建议使用 Guava Cache 或 Caffeine。

chapter 18:SoftValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

public class SoftValue<K, V> extends SoftReference<V> {

private final K key;

public SoftValue(K key, V value, ReferenceQueue<V> queue) {
super(value, queue);
this.key = key;
}

public K getKey() {
return key;
}
}

这里把 key 保存到 SoftValue 中。

当 value 被 GC 回收后,我们可以从 ReferenceQueue 中拿到 SoftValue,再根据 key 清理 Map。

chapter 19:InMemoryCache 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.lang.ref.ReferenceQueue;
import java.util.LinkedHashMap;
import java.util.Map;

public class InMemoryCache<K, V> {

private final int capacity;

private final ReferenceQueue<V> referenceQueue = new ReferenceQueue<>();

private final Map<K, SoftValue<K, V>> cache;

public InMemoryCache(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than 0");
}

this.capacity = capacity;

this.cache = new LinkedHashMap<>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, SoftValue<K, V>> eldest) {
return size() > InMemoryCache.this.capacity;
}
};
}

public synchronized void put(K key, V value) {
cleanUpClearedReferences();

cache.put(key, new SoftValue<>(key, value, referenceQueue));
}

public synchronized V get(K key) {
cleanUpClearedReferences();

SoftValue<K, V> reference = cache.get(key);

if (reference == null) {
return null;
}

V value = reference.get();

if (value == null) {
cache.remove(key);
}

return value;
}

public synchronized void remove(K key) {
cache.remove(key);
}

public synchronized int size() {
cleanUpClearedReferences();

return cache.size();
}

@SuppressWarnings("unchecked")
private void cleanUpClearedReferences() {
SoftValue<K, V> reference;

while ((reference = (SoftValue<K, V>) referenceQueue.poll()) != null) {
cache.remove(reference.getKey());
}
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InMemoryCacheDemo {

public static void main(String[] args) {
InMemoryCache<String, String> cache = new InMemoryCache<>(2);

cache.put("A", "value-A");
cache.put("B", "value-B");

cache.get("A");

cache.put("C", "value-C");

System.out.println(cache.get("A"));
System.out.println(cache.get("B"));
System.out.println(cache.get("C"));
}
}

chapter 20:手写缓存的问题

手写缓存看起来不难,但生产级缓存很复杂。

你还需要考虑:

  • 并发安全;
  • 加载回源;
  • 缓存击穿;
  • 缓存穿透;
  • 过期策略;
  • 驱逐策略;
  • 统计信息;
  • 删除通知;
  • 异步刷新;
  • 权重;
  • 内存占用;
  • 清理线程;
  • 异常处理。

所以:

手写缓存适合理解原理,不适合替代成熟缓存库。

chapter 21:Guava Cache 是什么

Guava Cache 是 Guava 提供的本地缓存工具。

核心类:

1
2
3
4
CacheBuilder
CacheLoader
LoadingCache
Cache

它支持:

  • 自动加载;
  • 最大容量;
  • 最大权重;
  • 写入后过期;
  • 访问后过期;
  • 弱 key;
  • 弱 value;
  • 软 value;
  • 删除通知;
  • 统计信息;
  • 刷新;
  • 预加载;
  • 手动失效。

它适合单 JVM 本地缓存。

chapter 22:CacheBuilder 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.TimeUnit;

public class CacheBuilderDemo {

public static void main(String[] args) throws Exception {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();

cache.put("name", "Mario");

System.out.println(cache.getIfPresent("name"));
}
}

Cache 不会自动加载数据。

如果 key 不存在:

1
cache.getIfPresent(key)

返回 null。

chapter 23:CacheLoader 和 LoadingCache

LoadingCache 可以自动加载数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.TimeUnit;

public class LoadingCacheDemo {

public static void main(String[] args) throws Exception {
LoadingCache<Long, String> userCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<Long, String>() {
@Override
public String load(Long userId) {
return loadUserName(userId);
}
});

System.out.println(userCache.get(1L));
System.out.println(userCache.get(1L));
}

private static String loadUserName(Long userId) {
System.out.println("加载用户,userId = " + userId);

return "user-" + userId;
}
}

第一次 get 会调用 load。

第二次 get 会命中缓存。

chapter 24:get 和 getUnchecked

LoadingCache 常用方法:

1
2
cache.get(key)
cache.getUnchecked(key)

get 会抛出 checked exception 包装异常:

1
ExecutionException

getUnchecked 会把异常包装成 unchecked exception。

示例:

1
2
3
4
5
try {
String value = cache.get(key);
} catch (ExecutionException e) {
throw new RuntimeException("load cache failed", e);
}

如果你确定 load 不会抛 checked exception,可以用:

1
cache.getUnchecked(key)

实践建议:

  • 业务代码希望明确处理加载失败,用 get;
  • 简单场景或不想处理 checked exception,用 getUnchecked;
  • load 中不要吞异常。

chapter 25:maximumSize 容量驱逐

1
2
3
LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(loader);

maximumSize 表示最多缓存多少条记录。

超过后会进行驱逐。

Guava Cache 的驱逐不是严格实时、精确每次立即发生,而是近似维护。

这对性能有好处。

适合绝大多数业务缓存。

chapter 26:maximumWeight 权重驱逐

如果每个缓存项大小差异很大,只按条目数量不合理。

例如:

  • 有的 value 很小;
  • 有的 value 很大;
  • 图片、报表、JSON 大小不同。

可以使用权重:

1
2
3
4
5
6
7
8
9
10
11
12
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.Weigher;

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumWeight(10000)
.weigher(new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return value.length();
}
})
.build(loader);

maximumWeight 必须配合 weigher 使用。

注意:

  • 权重是 int;
  • 权重计算要快;
  • 不要在 weigh 中做复杂逻辑;
  • 权重不是精确内存大小,只是业务估算值。

chapter 27:weakKeys

weakKeys() 表示缓存 key 使用弱引用。

1
2
3
CacheBuilder.newBuilder()
.weakKeys()
.build();

如果 key 没有其他强引用,GC 后缓存项可能失效。

适合:

  • key 是对象实例;
  • 不希望缓存阻止 key 被回收;
  • 框架元数据关联。

不适合:

  • 业务 ID key;
  • String key;
  • Long key;
  • 需要稳定缓存的场景。

因为弱 key 可能随 GC 消失。

chapter 28:weakValues 和 softValues

weakValues()

1
2
3
CacheBuilder.newBuilder()
.weakValues()
.build();

value 使用弱引用。

没有强引用时,GC 后可能回收。

softValues()

1
2
3
CacheBuilder.newBuilder()
.softValues()
.build();

value 使用软引用。

内存紧张时可能回收。

实践建议:

  • 不要随便使用 weakValues;
  • softValues 在现代缓存设计中也要谨慎;
  • 大多数业务缓存优先使用 maximumSize、expireAfterWrite;
  • 引用型缓存适合特殊内存敏感场景。

chapter 29:expireAfterWrite

写入后一段时间过期。

1
2
3
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();

含义:

1
缓存项写入后 10 分钟过期,不管有没有访问。

适合:

  • 数据有固定有效期;
  • 配置定期刷新;
  • 第三方接口结果短时间缓存;
  • 用户信息允许延迟刷新。

chapter 30:expireAfterAccess

访问后一段时间过期。

1
2
3
CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();

含义:

1
缓存项如果 10 分钟没有被访问,就过期。

适合:

  • 热点数据保留;
  • 冷数据淘汰;
  • 会话类缓存;
  • 用户上下文缓存。

对比:

策略 含义
expireAfterWrite 写入后固定时间过期
expireAfterAccess 最后访问后固定时间过期

chapter 31:Guava Cache 不允许缓存 null

CacheLoader.load() 不能返回 null。

如果返回 null,会抛异常。

为什么?

因为缓存里 null 语义不清楚:

1
2
3
4
是 key 不存在?
还是 value 就是 null?
是加载失败?
还是查不到数据?

Guava 选择禁止 null,避免歧义。

如果需要表达空值,可以使用:

  • Optional;
  • Null Object;
  • 自定义 NullValue。

chapter 32:NullValue 处理方案

定义 NullValue:

1
2
3
4
public enum NullValue {

INSTANCE
}

缓存类型:

1
2
3
4
5
6
7
8
9
10
LoadingCache<Long, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<Long, Object>() {
@Override
public Object load(Long key) {
User user = findUser(key);

return user == null ? NullValue.INSTANCE : user;
}
});

读取:

1
2
3
4
5
6
7
Object value = cache.getUnchecked(userId);

if (value == NullValue.INSTANCE) {
return null;
}

return (User) value;

更现代的写法可以缓存:

1
Optional<User>

例如:

1
LoadingCache<Long, Optional<User>> cache

chapter 33:RemovalListener

缓存项被移除时,可以收到通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.cache.*;

LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.removalListener(new RemovalListener<Long, String>() {
@Override
public void onRemoval(RemovalNotification<Long, String> notification) {
System.out.println("缓存移除,key = "
+ notification.getKey()
+ ",cause = "
+ notification.getCause());
}
})
.build(loader);

chapter 34:RemovalCause

常见 RemovalCause:

原因 说明
EXPLICIT 手动移除
REPLACED 被新值替换
COLLECTED 被 GC 回收
EXPIRED 过期
SIZE 超过容量或权重被驱逐

示例:

1
notification.getCause()

可以用于:

  • 日志记录;
  • 监控驱逐原因;
  • 释放外部资源;
  • 缓存调优。

注意:

RemovalListener 不要执行太重逻辑,避免影响缓存操作。

chapter 35:refreshAfterWrite

refreshAfterWrite 表示写入后一段时间允许刷新。

1
2
3
LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(loader);

它和 expire 不同。

expire 是过期后重新加载。

refresh 是旧值仍然可用,同时触发刷新。

适合:

  • 缓存希望后台刷新;
  • 不希望请求直接等待加载;
  • 数据允许短暂旧值。

需要注意:

  • 默认 reload 可能同步执行;
  • 可以重写 reload 实现异步刷新;
  • refresh 不等于定时主动刷新所有 key。

chapter 36:reload

1
2
3
4
5
6
7
8
9
10
11
12
13
LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<Long, String>() {
@Override
public String load(Long key) {
return loadValue(key);
}

@Override
public ListenableFuture<String> reload(Long key, String oldValue) {
return Futures.immediateFuture(loadValue(key));
}
});

如果要异步 reload,需要配合线程池。

在实际项目中,要小心:

  • reload 异常;
  • 下游压力;
  • 刷新风暴;
  • key 数量过多;
  • 旧值是否可接受。

chapter 37:预加载缓存

可以用:

1
2
cache.put(key, value)
cache.putAll(map)

示例:

1
2
3
4
5
6
Cache<Long, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build();

cache.put(1L, "Mario");
cache.put(2L, "Luigi");

适合:

  • 启动加载字典;
  • 预热热点数据;
  • 定时刷新配置;
  • 灰度前预加载。

但启动时预加载太多数据会拖慢启动速度。

要控制规模。

chapter 38:缓存穿透处理思路

缓存穿透是指:

请求的数据缓存没有,数据库也没有,导致每次请求都打到数据库。

处理思路:

1. 缓存空值

用 Optional 或 NullValue 缓存不存在结果。

2. 参数校验

非法 ID 直接拒绝。

3. 布隆过滤器

在访问缓存和数据库前判断 key 是否可能存在。

4. 限流

对异常 key 请求限速。

Guava Cache 可以缓存 Optional:

1
LoadingCache<Long, Optional<User>> cache

查不到用户时返回:

1
Optional.empty()

chapter 39:recordStats

开启统计:

1
2
3
4
LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.recordStats()
.build(loader);

获取统计:

1
CacheStats stats = cache.stats();

常见指标:

1
2
3
4
5
6
7
8
9
stats.hitCount()
stats.missCount()
stats.hitRate()
stats.missRate()
stats.loadSuccessCount()
stats.loadExceptionCount()
stats.totalLoadTime()
stats.averageLoadPenalty()
stats.evictionCount()

chapter 40:CacheStats 指标解释

指标 含义
hitCount 命中次数
missCount 未命中次数
hitRate 命中率
missRate 未命中率
loadSuccessCount 加载成功次数
loadExceptionCount 加载异常次数
totalLoadTime 总加载耗时
averageLoadPenalty 平均加载耗时
evictionCount 驱逐次数

这些指标可以帮助判断缓存效果。

例如:

  • 命中率低,说明缓存价值不高或过期太快;
  • 驱逐次数高,说明容量可能太小;
  • 加载耗时高,说明回源慢;
  • 加载异常多,说明下游不稳定。

chapter 41:CacheBuilderSpec

CacheBuilderSpec 可以用字符串配置 CacheBuilder。

示例:

1
2
3
4
5
6
7
8
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheBuilderSpec;

CacheBuilderSpec spec = CacheBuilderSpec.parse(
"maximumSize=1000,expireAfterWrite=10m,recordStats"
);

CacheBuilder<Object, Object> builder = CacheBuilder.from(spec);

适合配置化:

1
2
cache:
user: maximumSize=1000,expireAfterWrite=10m,recordStats

然后在代码里解析。

好处:

  • 缓存策略可配置;
  • 不用改代码;
  • 便于不同环境设置不同策略。

缺点:

  • 字符串配置容易写错;
  • IDE 不容易提示;
  • 配置项要做好校验。

chapter 42:Guava Cache 实战:用户缓存服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import com.google.common.cache.*;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

public class UserCacheService {

private final LoadingCache<Long, Optional<User>> cache;

public UserCacheService() {
this.cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.removalListener(new RemovalListener<Long, Optional<User>>() {
@Override
public void onRemoval(RemovalNotification<Long, Optional<User>> notification) {
System.out.println("用户缓存移除,key = "
+ notification.getKey()
+ ",cause = "
+ notification.getCause());
}
})
.build(new CacheLoader<Long, Optional<User>>() {
@Override
public Optional<User> load(Long userId) {
return Optional.ofNullable(loadUserFromDb(userId));
}
});
}

public Optional<User> getUser(Long userId) {
return cache.getUnchecked(userId);
}

public void invalidate(Long userId) {
cache.invalidate(userId);
}

public CacheStats stats() {
return cache.stats();
}

private User loadUserFromDb(Long userId) {
System.out.println("查询数据库,userId = " + userId);

if (userId <= 0) {
return null;
}

return new User(userId, "user-" + userId, true);
}
}

这个示例包含:

  • 自动加载;
  • Optional 缓存空值;
  • 最大容量;
  • 写入过期;
  • 删除通知;
  • 统计信息。

chapter 43:FluentIterable 是什么

FluentIterable 是 Guava 早期提供的链式集合处理工具。

在 Java 8 Stream 出现之前,它非常有用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.google.common.base.Predicate;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;

import java.util.List;

public class FluentIterableDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true),
new User(3L, "Peach", false)
);

List<String> names = FluentIterable.from(users)
.filter(new Predicate<User>() {
@Override
public boolean apply(User user) {
return user.isEnabled();
}
})
.transform(new Function<User, String>() {
@Override
public String apply(User user) {
return user.getUsername();
}
})
.toList();

System.out.println(names);
}
}

Java 8 之后,更推荐:

1
2
3
4
List<String> names = users.stream()
.filter(User::isEnabled)
.map(User::getUsername)
.toList();

chapter 44:FluentIterable 与 Stream 对比

对比项 FluentIterable Java Stream
出现时代 Java 8 之前 Java 8
filter 支持 支持
transform/map 支持 支持
first/last 支持 Stream 可实现
生态 Guava JDK 标准
推荐程度 老项目维护 新项目优先 Stream

如果维护老项目,要能看懂 FluentIterable。

新项目优先 Stream。

chapter 45:Lists 工具类

Guava Lists 常用方法:

1
2
3
4
5
6
Lists.newArrayList()
Lists.partition(list, size)
Lists.reverse(list)
Lists.transform(list, function)
Lists.cartesianProduct(...)
Lists.charactersOf(string)

newArrayList

1
2
3
4
5
6
7
8
9
10
11
12
import com.google.common.collect.Lists;

import java.util.ArrayList;

public class ListsDemo {

public static void main(String[] args) {
ArrayList<String> list = Lists.newArrayList("A", "B", "C");

System.out.println(list);
}
}

Java 9+ 有 List.of,但它返回不可变列表。

如果需要可变 ArrayList,Lists.newArrayList 仍然很方便。

chapter 46:Lists.partition

分批处理非常常见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.collect.Lists;

import java.util.List;

public class ListsPartitionDemo {

public static void main(String[] args) {
List<Integer> ids = List.of(1, 2, 3, 4, 5, 6, 7);

List<List<Integer>> partitions = Lists.partition(ids, 3);

System.out.println(partitions);
}
}

输出:

1
[[1, 2, 3], [4, 5, 6], [7]]

适合:

  • 批量查询;
  • 批量插入;
  • 分批调用接口;
  • 分批发送消息。

chapter 47:Lists.reverse、transform、cartesianProduct、charactersOf

reverse

1
List<Integer> reversed = Lists.reverse(ids);

注意返回的是视图,不一定是新列表。

transform

1
List<String> names = Lists.transform(users, User::getUsername);

Java 8 后推荐 Stream。

cartesianProduct

笛卡尔积:

1
2
3
4
List<List<String>> result = Lists.cartesianProduct(
List.of("S", "M"),
List.of("Red", "Blue")
);

输出:

1
[[S, Red], [S, Blue], [M, Red], [M, Blue]]

适合 SKU 组合。

charactersOf

1
List<Character> chars = Lists.charactersOf("abc");

chapter 48:Sets 工具类

Guava Sets 常用能力:

1
2
3
4
5
6
7
Sets.newHashSet()
Sets.union(a, b)
Sets.intersection(a, b)
Sets.difference(a, b)
Sets.symmetricDifference(a, b)
Sets.combinations(set, size)
Sets.powerSet(set)

chapter 49:Sets 集合运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.collect.Sets;

import java.util.Set;

public class SetsOperationDemo {

public static void main(String[] args) {
Set<String> a = Set.of("A", "B", "C");
Set<String> b = Set.of("B", "C", "D");

System.out.println(Sets.union(a, b));
System.out.println(Sets.intersection(a, b));
System.out.println(Sets.difference(a, b));
System.out.println(Sets.symmetricDifference(a, b));
}
}

含义:

方法 含义
union 并集
intersection 交集
difference 差集
symmetricDifference 对称差集

常见业务:

  • 权限对比;
  • 标签差异;
  • 用户分组差异;
  • 数据同步差异;
  • 新增/删除集合计算。

chapter 50:Sets.combinations 和 powerSet

组合:

1
Set<Set<String>> combinations = Sets.combinations(Set.of("A", "B", "C"), 2);

结果:

1
2
3
[A, B]
[A, C]
[B, C]

幂集:

1
Set<Set<String>> powerSet = Sets.powerSet(Set.of("A", "B", "C"));

幂集会生成所有子集。

注意:

powerSet 数据量是 2^n,n 大时非常危险。

不要对大集合使用 powerSet。

chapter 51:Maps 工具类

Guava Maps 常用能力:

1
2
3
4
5
6
Maps.newHashMap()
Maps.uniqueIndex(...)
Maps.difference(...)
Maps.transformValues(...)
Maps.filterKeys(...)
Maps.filterValues(...)

chapter 52:Maps.uniqueIndex

根据对象字段建立索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.collect.Maps;

import java.util.List;
import java.util.Map;

public class UniqueIndexDemo {

public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Mario", true),
new User(2L, "Luigi", true)
);

Map<Long, User> userMap = Maps.uniqueIndex(users, User::getId);

System.out.println(userMap);
}
}

注意:

key 必须唯一,否则会抛异常。

适合:

  • List 转 Map;
  • 按 ID 建索引;
  • 批量查询结果映射;
  • 后续快速查找。

Java Stream 也可以:

1
users.stream().collect(Collectors.toMap(User::getId, Function.identity()))

chapter 53:Maps.difference

比较两个 Map 的差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;

import java.util.Map;

public class MapsDifferenceDemo {

public static void main(String[] args) {
Map<String, String> oldMap = Map.of(
"name", "Mario",
"city", "Hangzhou"
);

Map<String, String> newMap = Map.of(
"name", "Mario",
"city", "Shanghai",
"level", "VIP"
);

MapDifference<String, String> difference = Maps.difference(oldMap, newMap);

System.out.println("只在左边:" + difference.entriesOnlyOnLeft());
System.out.println("只在右边:" + difference.entriesOnlyOnRight());
System.out.println("相同:" + difference.entriesInCommon());
System.out.println("不同:" + difference.entriesDiffering());
}
}

适合:

  • 配置变更对比;
  • 表单修改对比;
  • 审计日志;
  • 数据同步差异。

chapter 54:BiMap 双向映射

BiMap 表示双向唯一映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

public class BiMapDemo {

public static void main(String[] args) {
BiMap<String, Integer> statusMap = HashBiMap.create();

statusMap.put("CREATED", 1);
statusMap.put("PAID", 2);

System.out.println(statusMap.get("PAID"));

BiMap<Integer, String> inverse = statusMap.inverse();

System.out.println(inverse.get(2));
}
}

注意:

BiMap 的 value 也必须唯一。

适合:

  • 状态码和状态名映射;
  • 枚举编码映射;
  • 双向查找。

chapter 55:Multimap 一键多值

普通 Map:

1
Map<String, List<User>>

写起来很啰嗦。

Guava 提供 Multimap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

public class MultimapDemo {

public static void main(String[] args) {
Multimap<String, String> roleUsers = ArrayListMultimap.create();

roleUsers.put("admin", "Mario");
roleUsers.put("admin", "Luigi");
roleUsers.put("user", "Peach");

System.out.println(roleUsers.get("admin"));
}
}

输出:

1
[Mario, Luigi]

常见实现:

实现 特点
ArrayListMultimap value 可重复,有序
HashMultimap value 去重,无序
LinkedListMultimap 保持插入顺序
TreeMultimap key/value 排序

chapter 56:Multimap 实战:用户按角色分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

import java.util.List;

public class UserRoleGroupDemo {

public static void main(String[] args) {
List<UserRole> userRoles = List.of(
new UserRole("Mario", "admin"),
new UserRole("Luigi", "admin"),
new UserRole("Peach", "user")
);

Multimap<String, String> roleUserMap = ArrayListMultimap.create();

for (UserRole userRole : userRoles) {
roleUserMap.put(userRole.getRole(), userRole.getUsername());
}

System.out.println(roleUserMap.get("admin"));
}
}

辅助类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserRole {

private final String username;

private final String role;

public UserRole(String username, String role) {
this.username = username;
this.role = role;
}

public String getUsername() {
return username;
}

public String getRole() {
return role;
}
}

Java Stream 也可以分组:

1
Collectors.groupingBy(...)

但 Multimap 在某些需要频繁 put 的场景很方便。

chapter 57:Table 二维表结构

Table<R, C, V> 表示二维表。

类似:

1
rowKey + columnKey -> value

例如学生成绩:

1
student + subject -> score

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;

public class TableDemo {

public static void main(String[] args) {
Table<String, String, Integer> scoreTable = HashBasedTable.create();

scoreTable.put("Mario", "Math", 90);
scoreTable.put("Mario", "English", 85);
scoreTable.put("Luigi", "Math", 88);

System.out.println(scoreTable.get("Mario", "Math"));
System.out.println(scoreTable.row("Mario"));
System.out.println(scoreTable.column("Math"));
System.out.println(scoreTable.cellSet());
}
}

适合:

  • 二维配置;
  • 价格矩阵;
  • 学生成绩;
  • 地区-品类指标;
  • 行列报表。

chapter 58:Range 区间表达

Range 用于表达区间。

常见方法:

1
2
3
4
5
6
Range.closed(1, 10)      // [1,10]
Range.open(1, 10) // (1,10)
Range.closedOpen(1, 10) // [1,10)
Range.openClosed(1, 10) // (1,10]
Range.atLeast(1) // [1,+∞)
Range.lessThan(10) // (-∞,10)

示例:

1
2
3
4
5
6
7
8
9
10
11
import com.google.common.collect.Range;

public class RangeDemo {

public static void main(String[] args) {
Range<Integer> range = Range.closedOpen(1, 10);

System.out.println(range.contains(1));
System.out.println(range.contains(10));
}
}

输出:

1
2
true
false

chapter 59:RangeSet 区间集合

RangeSet 表示多个区间的集合,并且会自动合并相连或重叠区间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;

public class RangeSetDemo {

public static void main(String[] args) {
RangeSet<Integer> rangeSet = TreeRangeSet.create();

rangeSet.add(Range.closed(1, 10));
rangeSet.add(Range.closed(8, 20));

System.out.println(rangeSet);
System.out.println(rangeSet.contains(15));
}
}

输出:

1
2
[[1..20]]
true

适合:

  • 时间段合并;
  • IP 段;
  • 价格区间;
  • 年龄区间;
  • 权限范围。

chapter 60:RangeMap 区间映射

RangeMap 表示:

1
区间 -> 值

例如会员等级:

1
2
3
[0,100)     -> BRONZE
[100,500) -> SILVER
[500,+∞) -> GOLD

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.TreeRangeMap;

public class RangeMapDemo {

public static void main(String[] args) {
RangeMap<Integer, String> levelMap = TreeRangeMap.create();

levelMap.put(Range.closedOpen(0, 100), "BRONZE");
levelMap.put(Range.closedOpen(100, 500), "SILVER");
levelMap.put(Range.atLeast(500), "GOLD");

System.out.println(levelMap.get(50));
System.out.println(levelMap.get(200));
System.out.println(levelMap.get(800));
}
}

输出:

1
2
3
BRONZE
SILVER
GOLD

chapter 61:RangeMap 实战:价格区间匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.TreeRangeMap;

import java.math.BigDecimal;

public class PriceRangeDiscountDemo {

public static void main(String[] args) {
RangeMap<BigDecimal, String> discountMap = TreeRangeMap.create();

discountMap.put(Range.closedOpen(new BigDecimal("0"), new BigDecimal("100")), "NO_DISCOUNT");
discountMap.put(Range.closedOpen(new BigDecimal("100"), new BigDecimal("500")), "DISCOUNT_10");
discountMap.put(Range.atLeast(new BigDecimal("500")), "DISCOUNT_50");

System.out.println(discountMap.get(new BigDecimal("80")));
System.out.println(discountMap.get(new BigDecimal("300")));
System.out.println(discountMap.get(new BigDecimal("800")));
}
}

适合:

  • 价格区间;
  • 时间区间;
  • 积分等级;
  • 会员等级;
  • 风险评分;
  • 订单金额分层。

chapter 62:Immutable Collections

Guava 的不可变集合非常经典。

常见类型:

1
2
3
4
5
ImmutableList
ImmutableSet
ImmutableMap
ImmutableMultimap
ImmutableTable

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

public class ImmutableCollectionsDemo {

public static void main(String[] args) {
ImmutableList<String> names = ImmutableList.of("Mario", "Luigi", "Peach");

ImmutableMap<String, Integer> statusMap = ImmutableMap.of(
"CREATED", 1,
"PAID", 2
);

System.out.println(names);
System.out.println(statusMap);
}
}

不可变集合创建后不能修改。

如果调用:

1
names.add("Bowser")

会抛异常。

chapter 63:不可变集合的优势

1. 线程安全

不可变对象天然线程安全。

多个线程读不需要加锁。

2. 防御性复制

避免外部修改内部集合。

1
this.roles = ImmutableList.copyOf(roles);

3. 表达语义明确

表示这个集合不应该被修改。

4. 减少 bug

避免方法拿到集合后偷偷修改。

5. 适合常量配置

例如状态码、权限码、白名单。

chapter 64:ImmutableList.copyOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.collect.ImmutableList;

import java.util.List;

public class ImmutableCopyDemo {

public static void main(String[] args) {
List<String> mutable = List.of("A", "B", "C");

ImmutableList<String> immutable = ImmutableList.copyOf(mutable);

System.out.println(immutable);
}
}

Java 9+ 也有:

1
2
3
List.of(...)
Map.of(...)
Set.of(...)

但 Guava 的 Immutable Collections 仍然提供了更丰富的不可变集合类型,例如 ImmutableMultimap、ImmutableTable。

chapter 65:Sorted Collections 和 Ordering

Guava 提供 Ordering 作为早期比较器工具。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.collect.Ordering;

import java.util.List;

public class OrderingDemo {

public static void main(String[] args) {
List<Integer> numbers = List.of(3, 1, 2);

List<Integer> sorted = Ordering.natural().sortedCopy(numbers);

System.out.println(sorted);
}
}

Java 8 后,更多使用:

1
2
Comparator
Stream.sorted()

例如:

1
numbers.stream().sorted().toList()

Guava 的 Ordering 在老项目中常见。

新项目优先 JDK Comparator。

chapter 66:不可变集合与线程安全

不可变集合的线程安全来自:

1
状态不可变。

如果集合内部元素本身也是不可变对象,那么整体非常安全。

但要注意:

1
ImmutableList<User>

如果 User 是可变对象,集合结构不可变,不代表 User 内部不可变。

例如:

1
2
不能 add/remove User
但 User 自己的字段可能被改

所以真正线程安全要看:

  • 集合是否不可变;
  • 元素是否不可变;
  • 是否存在外部共享可变状态。

chapter 67:Guava Collections 和 Java Stream 怎么选

场景 推荐
过滤转换 Java Stream
分批 partition Guava Lists.partition
集合交并差 Guava Sets
一键多值 Guava Multimap
双向映射 Guava BiMap
二维表 Guava Table
区间表达 Guava Range / RangeSet / RangeMap
不可变多值集合 Guava ImmutableMultimap
普通不可变 List/Map Java List.of / Map.of 或 Guava
排序 Java Comparator / Stream

Guava Collections 的优势在于:

它提供了很多 JDK 没有的集合结构。

比如:

1
2
3
4
5
6
BiMap
Multimap
Table
RangeSet
RangeMap
ImmutableMultimap

这些在业务建模中很有价值。

chapter 68:第九章使用建议

1. 简单 LRU 用 LinkedHashMap

学习和小工具足够。

2. 业务缓存用 Guava Cache 或更现代缓存库

不要手写复杂缓存用于生产核心链路。

3. 引用类型缓存要谨慎

SoftReference、WeakReference 不适合大多数稳定业务缓存。

4. Guava Cache 不允许 null

用 Optional 或 NullValue 表达空值。

5. 缓存必须监控

开启:

1
recordStats()

观察命中率和驱逐情况。

6. 本地缓存不是分布式缓存

多实例部署要注意一致性。

7. 集合工具要按场景选

Java Stream 很强,但不是所有集合建模问题都适合 Stream。

8. 不可变集合非常推荐

尤其适合配置、常量、DTO 防御性复制。

9. RangeMap 很适合区间匹配

比一堆 if else 更清晰。

10. Multimap 很适合一键多值

Map<K, List<V>> 更优雅。

chapter 69:一句话总结

这一章的核心是:

缓存解决的是高频读取和重复计算问题,引用类型影响对象生命周期,Guava Cache 提供了成熟的单机缓存能力,Guava Collections 提供了大量 JDK 没有的高级集合结构。

LRU 的核心是:

1
最近使用的数据保留,最久未使用的数据淘汰。

引用类型的核心是:

1
不同引用强度决定对象被 GC 回收的时机。

Guava Cache 的核心是:

1
用 CacheBuilder 声明缓存策略,用 CacheLoader 加载数据,用 LoadingCache 统一访问缓存。

Guava Collections 的核心是:

1
用更贴近业务语义的数据结构表达复杂集合关系。

例如:

  • Multimap 表达一键多值;
  • BiMap 表达双向唯一映射;
  • Table 表达二维表;
  • RangeMap 表达区间到值;
  • ImmutableList 表达不可变集合。

好代码不是把所有数据都塞进 Map<String, Object>

好代码应该用合适的数据结构表达业务语义。

缓存也是一样。

好缓存不是随便加一层 Map。

好缓存要考虑:

1
2
3
4
5
6
7
8
9
10
容量
过期
驱逐
加载
空值
刷新
统计
并发
一致性
可观测性

这才是缓存真正的工程难点。

参考资料

  • Google Guava API: CacheBuilder.
  • Google Guava API: CacheLoader.
  • Google Guava API: LoadingCache.
  • Google Guava API: CacheStats.
  • Google Guava API: RemovalListener.
  • Google Guava API: RemovalCause.
  • Google Guava API: FluentIterable.
  • Google Guava API: Lists.
  • Google Guava API: Sets.
  • Google Guava API: Maps.
  • Google Guava API: BiMap.
  • Google Guava API: Multimap.
  • Google Guava API: Table.
  • Google Guava API: Range.
  • Google Guava API: RangeSet.
  • Google Guava API: RangeMap.
  • Google Guava API: Immutable Collections.
  • Java Documentation: Reference.
  • Java Documentation: SoftReference.
  • Java Documentation: WeakReference.
  • Java Documentation: PhantomReference.
  • Java Documentation: ReferenceQueue.
  • Joshua Bloch. Effective Java.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Guava:缓存、引用类型与集合工具
https://allendericdalexander.github.io/2026/04/01/java/utils/guava/09guava-cache-reference-collections-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议