我遇到了一个有趣的问题,想知道是否以及如何在 Java 中完成此操作: 创建一个可以记住任何函数/方法的方法。该方法具有以下参数:方法/函数及其参数。
例如,假设我有这个方法:
int addOne(int a) { return a + 1;}
我使用相同的参数调用我的记忆方法两次:例如 addOne 和 5,第一次调用实际上应该调用 addOne 方法并返回结果,并存储给定参数的结果。当我第二次打电话时,它应该知道之前已经被调用过,只需查找以前的答案即可。
我的想法是拥有类似
HashMap<Callable,HashMap<List<Objects>,Object>>
的东西,您可以在其中存储以前的答案并稍后查找它们。我认为这可以通过 lambda 表达式以某种方式完成,但我对它们不太熟悉。我不太确定如何编写这个方法,希望得到一些帮助。
用这种方法可以做到吗?
ConcurrentHashMap.computeIfAbsent
:
Map<Integer, Integer> cache = new ConcurrentHashMap<>();
Integer addOne(Integer x) {
return cache.computeIfAbsent(x -> x + 1);
}
DZone 有 一个很好的教程,它提供了适用于任何方法的解决方案:
类非常简单:Memoizer
public class Memoizer<T, U> { private final Map<T, U> cache = new ConcurrentHashMap<>(); private Memoizer() {} private Function<T, U> doMemoize(final Function<T, U> function) { return input -> cache.computeIfAbsent(input, function::apply); } public static <T, U> Function<T, U> memoize(final Function<T, U> function) { return new Memoizer<T, U>().doMemoize(function); } }
使用这个类也极其简单:
Integer longCalculation(Integer x) { try { Thread.sleep(1_000); } catch (InterruptedException ignored) { } return x * 2; } Function<Integer, Integer> f = this::longCalculation; Function<Integer, Integer> g = Memoizer.memoize(f); public void automaticMemoizationExample() { long startTime = System.currentTimeMillis(); Integer result1 = g.apply(1); long time1 = System.currentTimeMillis() - startTime; startTime = System.currentTimeMillis(); Integer result2 = g.apply(1); long time2 = System.currentTimeMillis() - startTime; System.out.println(result1); System.out.println(result2); System.out.println(time1); System.out.println(time2); }
运行
方法将产生以下结果:automaticMemoizationExample
2 2 1000 0
如果您愿意放弃参数的类型安全性,您可以使用 Java 8 的
MethodHandle
和 lambda 来记忆任何函数:
public interface MemoizedFunction<V> {
V call(Object... args);
}
private static class ArgList {
public Object[] args;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ArgList)) {
return false;
}
ArgList argList = (ArgList) o;
// Probably incorrect - comparing Object[] arrays with Arrays.equals
return Arrays.equals(args, argList.args);
}
@Override
public int hashCode() {
return args != null ? Arrays.hashCode(args) : 0;
}
}
public static <V> MemoizedFunction<V> memoizeFunction(Class<? super V> returnType, Method method) throws
IllegalAccessException {
final Map<ArgList, V> memoizedCalls = new HashMap<>();
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.unreflect(method)
.asSpreader(Object[].class, method.getParameterCount());
return args -> {
ArgList argList = new ArgList();
argList.args = args;
return memoizedCalls.computeIfAbsent(argList, argList2 -> {
try {
//noinspection unchecked
return (V) methodHandle.invoke(args);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
};
}
这会创建一个包含该函数的可变参数 lambda,并且在构造 lambda 后几乎与直接调用该函数一样快(即,在
call(Object...args)
内部不会发生反射),因为我们使用的是 MethodHandle.invoke()
而不是 Method.invoke()
。
您仍然可以在没有 lambda(用匿名类替换)和 MethodHandles(用 Method.invoke 替换)的情况下执行此操作,但是会有性能损失,这使得这对于注重性能的代码不太有吸引力。
当您要求特定的方法来完成您的任务时,有一种 OOP+FP(面向对象编程与函数式编程集成)方法来解决问题。虽然我在下面对其进行了简化,但它支持多种方式来“通用地实现 memoize”。
我发现它在我的项目中特别有用。例如,我通过围绕各种
enum
生成“缓存”助手来利用它。例如,可以轻松地通过 enum
查找 String
值,同时忽略大小写。另一个示例是通过 enum
或数据库 ID 轻松查找 .ordinal()
值。
与我见过的建议的其他解决方案相比,这里列出了为客户带来的好处:
keySet().iterator()
内的插入顺序(对于调试会话来说非常好)public final class Memoizer<K, V> {
private final Map<K, V> vByK;
private final Optional<Function<K, V>> optionalDefaultDeriveVFromK;
public static <K, V> Memoizer<K, V> from() {
return from(Optional.empty());
}
public static <K, V> Memoizer<K, V> from(
Optional<Function<K, V>> optionalDefaultDeriveVFromK
) {
return new Memoizer<>(optionalDefaultDeriveVFromK, false);
}
public static <K, V> Memoizer<K, V> from(
boolean isRetainingInsertionOrder
) {
return new Memoizer<>(Optional.empty(), isRetainingInsertionOrder);
}
private Memoizer(
Optional<Function<K, V>> optionalDefaultDeriveVFromK,
boolean isRetainingInsertionOrder
) {
this.optionalDefaultDeriveVFromK = Objects.requireNonNull(optionalDefaultDeriveVFromK);
this.vByK = isRetainingInsertionOrder
? new LinkedHashMap<>()
: new HashMap<>();
}
public V get(
K k,
Optional<Function<K, V>> optionalOverrideDeriveVFromK
) {
return Optional.ofNullable(vByK.get(Objects.requireNonNull(k, "k must not be null")))
.orElseGet(() -> {
synchronized (vByK) {
return Optional.ofNullable(vByK.get(k))
.orElseGet(() -> {
var lambda = optionalOverrideDeriveVFromK
.or(() -> optionalDefaultDeriveVFromK)
.orElseThrow(() -> new IllegalArgumentException(
"when optionalDeriveVFromK is empty, optionalOverrideDeriveVFromK must not be empty"));
var v = Objects.requireNonNull(lambda.apply(k),
"v returned from %s function must not be null".formatted(
optionalOverrideDeriveVFromK.isEmpty()
? "optionalDefaultDeriveVFromK"
: "optionalOverrideDeriveVFromK"));
vByK.put(k, v);
return v;
});
}
});
}
public V get(K k) {
return get(k, Optional.empty());
}
public Set<K> keySet() {
return Collections.unmodifiableSet(vByK.keySet());
}
}