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:基于时间的淘汰策略

调用返回的 Supplierget() 方法时:

  • 若结果已存在内存:直接返回缓存值
  • 若结果不存在:执行目标方法并缓存结果

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 记忆化

对单参数方法进行记忆化时,需:

  1. 使用 CacheLoader.from() 将方法转换为 Guava Function
  2. 构建 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 仓库


原始标题:Introduction to Guava Memoizer