1. 概述
本文将深入探讨 Google Guava 库中的记忆化(Memoization)功能。记忆化是一种优化技术,通过缓存函数首次执行的结果来避免重复执行计算成本高昂的操作。
1.1. 记忆化 vs 缓存
记忆化与缓存在内存存储层面相似,两者都通过减少对计算密集型代码的调用来提升效率。但存在关键区别:
- 缓存:更通用的概念,作用于类实例化、对象检索或内容检索层面
- 记忆化:专门解决方法/函数执行层面的性能问题
1.2. Guava Memoizer 与 Guava Cache
Guava 同时支持记忆化和缓存功能,但需注意:
✅ 记忆化适用场景:
- 无参函数(
Supplier
接口) - 单参数函数(
Function
接口)
⚠️ 当前限制(截至 Guava 23.6):
- 不支持多参数函数的记忆化
可通过按需调用记忆化 API 并配置淘汰策略来控制内存中的条目数量,防止内存无限增长。函数记忆化可通过 Guava Cache 实现(详见第 3 节)。
2. Supplier 记忆化
Suppliers
类提供两个核心记忆化方法:
memoize
:无淘汰策略memoizeWithExpiration
:基于时间的淘汰策略
调用返回的 Supplier
的 get()
方法时:
- 若结果已存在内存:直接返回缓存值
- 若结果不存在:执行目标方法并缓存结果
2.1. 无淘汰策略的 Supplier 记忆化
使用 Suppliers.memoize()
方法,传入方法引用:
Supplier<String> memoizedSupplier = Suppliers.memoize(
CostlySupplier::generateBigNumber);
⚠️ 重要特性:
- 首次调用
get()
后,结果将永久驻留内存 - 后续所有
get()
调用直接返回缓存值 - 应用程序运行期间缓存永不失效
2.2. 基于 TTL 的 Supplier 记忆化
当需要限制缓存有效期时,使用 memoizeWithExpiration
:
Supplier<String> memoizedSupplier = Suppliers.memoizeWithExpiration(
CostlySupplier::generateBigNumber, 5, TimeUnit.SECONDS);
✅ 行为特点:
- 指定时间(如 5 秒)后自动淘汰缓存
- 超时后首次调用将重新执行目标方法
- 支持所有
TimeUnit
时间单位(秒/分钟等)
2.3. 实战示例
模拟一个计算密集型方法 generateBigNumber
:
public class CostlySupplier {
private static BigInteger generateBigNumber() {
try {
TimeUnit.SECONDS.sleep(2); // 模拟耗时操作
} catch (InterruptedException e) {}
return new BigInteger("12345");
}
}
测试记忆化效果(省略淘汰策略):
@Test
public void givenMemoizedSupplier_whenGet_thenSubsequentGetsAreFast() {
Supplier<BigInteger> memoizedSupplier;
memoizedSupplier = Suppliers.memoize(CostlySupplier::generateBigNumber);
BigInteger expectedValue = new BigInteger("12345");
assertSupplierGetExecutionResultAndDuration(
memoizedSupplier, expectedValue, 2000D); // 首次调用耗时 2 秒
assertSupplierGetExecutionResultAndDuration(
memoizedSupplier, expectedValue, 0D); // 后续调用接近 0 毫秒
assertSupplierGetExecutionResultAndDuration(
memoizedSupplier, expectedValue, 0D);
}
private <T> void assertSupplierGetExecutionResultAndDuration(
Supplier<T> supplier, T expectedValue, double expectedDurationInMs) {
Instant start = Instant.now();
T value = supplier.get();
Long durationInMs = Duration.between(start, Instant.now()).toMillis();
double marginOfErrorInMs = 100D;
assertThat(value, is(equalTo(expectedValue)));
assertThat(
durationInMs.doubleValue(),
is(closeTo(expectedDurationInMs, marginOfErrorInMs)));
}
📊 关键结论:
- 首次调用
get()
耗时 2 秒(模拟计算时间) - 后续调用因记忆化效果耗时接近 0 毫秒
- 显著提升重复调用性能
3. Function 记忆化
对单参数方法进行记忆化时,需:
- 使用
CacheLoader.from()
将方法转换为 GuavaFunction
- 构建
LoadingCache
映射结构
LoadingCache<Integer, BigInteger> memo = CacheBuilder.newBuilder()
.build(CacheLoader.from(FibonacciSequence::getFibonacciNumber));
⚠️ 核心约束:
LoadingCache
是并发映射,不允许 null 键或值- 确保目标方法不支持 null 参数且不返回 null
3.1. 带淘汰策略的 Function 记忆化
可应用 Guava Cache 的各种淘汰策略,例如:
LoadingCache<Integer, BigInteger> memo = CacheBuilder.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS) // 2 秒未访问则淘汰
.build(CacheLoader.from(Fibonacci::getFibonacciNumber));
📋 常用淘汰策略:
expireAfterWrite
:写入后固定时间淘汰expireAfterAccess
:访问后固定时间淘汰maximumSize
:基于最大条目数淘汰weakKeys/weakValues
:基于弱引用淘汰
3.2. 斐波那契数列示例
递归实现斐波那契数列计算:
public static BigInteger getFibonacciNumber(int n) {
if (n == 0) {
return BigInteger.ZERO;
} else if (n == 1) {
return BigInteger.ONE;
} else {
return getFibonacciNumber(n - 1).add(getFibonacciNumber(n - 2));
}
}
❌ 未优化问题:
- 输入值较大时性能急剧下降
- 存在大量重复计算
✅ 记忆化优化方案(限制缓存 100 条):
public class FibonacciSequence {
private static LoadingCache<Integer, BigInteger> memo = CacheBuilder.newBuilder()
.maximumSize(100) // 最多缓存 100 个结果
.build(CacheLoader.from(FibonacciSequence::getFibonacciNumber));
public static BigInteger getFibonacciNumber(int n) {
if (n == 0) {
return BigInteger.ZERO;
} else if (n == 1) {
return BigInteger.ONE;
} else {
// 使用 getUnchecked 避免检查异常
return memo.getUnchecked(n - 1).add(memo.getUnchecked(n - 2));
}
}
}
💡 关键技巧:
- 使用
getUnchecked()
简化异常处理 - 递归调用改为从缓存获取中间结果
- 通过
maximumSize
防止内存溢出
3.3. 阶乘计算示例
递归实现阶乘计算:
public static BigInteger getFactorial(int n) {
if (n == 0) {
return BigInteger.ONE;
} else {
return BigInteger.valueOf(n).multiply(getFactorial(n - 1));
}
}
✅ 记忆化优化版本:
public class Factorial {
private static LoadingCache<Integer, BigInteger> memo = CacheBuilder.newBuilder()
.build(CacheLoader.from(Factorial::getFactorial));
public static BigInteger getFactorial(int n) {
if (n == 0) {
return BigInteger.ONE;
} else {
return BigInteger.valueOf(n).multiply(memo.getUnchecked(n - 1));
}
}
}
📈 性能提升:
- 避免重复计算中间结果
- 大数阶乘计算速度显著提升
- 内存占用可控
4. 总结
本文系统介绍了 Guava 提供的记忆化能力:
✅ 核心能力:
Supplier
记忆化(无参/带TTL)Function
记忆化(单参数)- 灵活的淘汰策略配置
⚠️ 使用注意:
- 仅适用于纯函数(相同输入必产生相同输出)
- 需合理配置淘汰策略防止内存泄漏
- 多参数函数需自行转换或使用其他方案
完整示例代码请查阅 GitHub 仓库。