Memoization(記憶化)源自 Lisp/Python/Perl 等函數式語言,核心思想:緩存純函數的計算結果,相同輸入直接返回緩存值。適用於計算密集型、冪等、無副作用的方法(如遞歸斐波那契、數學函數、昂貴的數據庫聚合查詢)。
public class PiCalculator {
private Map<Integer, Byte> cache = new HashMap<>();
public synchronized byte calculate(int n) {
if (cache.containsKey(n)) return cache.get(n);
byte result = runBBPAlgorithm(n); // 耗時計算
cache.put(n, result);
return result;
}
}
缺點:業務邏輯與緩存邏輯耦合、難以單元測試(無法驗證算法本身)、緩存策略難以替換、違反單一職責原則。
public interface BinaryDigitsCalculator {
byte calculateBinaryDigit(int n);
}
public class PiCalculator implements BinaryDigitsCalculator {
public byte calculateBinaryDigit(int n) { return runBBPAlgorithm(n); }
}
public class CachingCalculator implements BinaryDigitsCalculator {
private final BinaryDigitsCalculator delegate;
private final Map<Integer, Byte> cache = new ConcurrentHashMap<>();
public CachingCalculator(BinaryDigitsCalculator delegate) { this.delegate = delegate; }
public byte calculateBinaryDigit(int n) {
return cache.computeIfAbsent(n, delegate::calculateBinaryDigit);
}
}
優勢:計算邏輯純淨可測、緩存可熱插拔、支持多種緩存策略(LRU、TTL、弱引用)組合。
public final class Memoizer {
private Memoizer() {}
@SuppressWarnings("unchecked")
public static <T> T memoize(T target) {
Class<?>[] interfaces = target.getClass().getInterfaces();
if (interfaces.length != 1) {
throw new IllegalArgumentException("Target must implement exactly one interface");
}
Class<T> iface = (Class<T>) interfaces[0];
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class[]{iface},
new MemoizationInvocationHandler(target)
);
}
private static class MemoizationInvocationHandler implements InvocationHandler {
private final Object target;
private final Map<MethodKey, Object> cache = new ConcurrentHashMap<>();
MemoizationInvocationHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 僅緩存無參或所有參數可序列化/可比較的方法
MethodKey key = new MethodKey(method, args);
return cache.computeIfAbsent(key, k -> {
try { return method.invoke(target, args); }
catch (InvocationTargetException e) { throw e.getCause(); }
catch (Exception e) { throw new UndeclaredThrowableException(e); }
});
}
}
// 方法簽名 + 參數值 作為 Key,需正確實現 equals/hashCode
private static final class MethodKey {
private final Method method;
private final Object[] args;
// ... equals/hashCode 基於 method + Arrays.deepEquals(args)
}
}
public interface ExpensiveService {
BigDecimal computeRiskScore(String portfolioId, LocalDate asOf);
}
ExpensiveService real = new ExpensiveServiceImpl();
ExpensiveService cached = Memoizer.memoize(real); // 透明添加緩存
// 業務代碼無感知
BigDecimal score = cached.computeRiskScore("PF001", LocalDate.now());
| 議題 | 解決方案 |
|---|---|
| 緩存無限增長 | 引入 Cache<K,V> 抽象(Guava Cache / Caffeine / Ehcache),支持最大條目、TTL、基於權重驅逐。 |
| 參數包含不可比較對象 | 要求參數實現 equals/hashCode,或提供 KeyGenerator 策略接口(如 JSON 序列化、SpEL 表達式)。 |
| 線程安全與併發穿透 | ConcurrentHashMap.computeIfAbsent 原子性保證單次計算;高併發熱點可用 Future 模式或 StampedLock 優化。 |
| 異常結果不應緩存 | 捕獲異常並拋出,不寫入緩存;或引入 CacheLoader 區分「計算失敗」與「計算結果為 null」。 |
| 方法副作用/非冪等 | 嚴禁對有副作用方法使用 Memoization;可通過註解 @Memoizable 標記並編譯期檢查。 |
| 分佈式環境一致性 | 單機緩存不適用;改用 Redis + Lua 腳本實現分佈式「計算一次」語義,或引入分佈式鎖。 |
get(key, callable)。動態代理實現的通用 Memoizer,以裝飾器模式 + 接口導向設計,將「緩存」這一橫切關注點從業務邏輯中完全剝離。它展示了 Java 元編程能力在解決橫切關注點時的優雅性:零侵入、可組合、可測試、可替換。生產環境建議在此基礎上集成成熟緩存庫(Caffeine/Redis),並嚴格限定適用於純計算、冪等、參數可比較的接口方法。