【问题标题】:Java memoization methodJava 记忆方法
【发布时间】:2015-02-17 10:25:14
【问题描述】:

我遇到了一个有趣的问题,想知道是否以及如何在 Java 中做到这一点: 创建一个可以记忆任何函数/方法的方法。该方法具有以下参数:方法/函数及其参数。

例如,假设我有这个方法:

int addOne(int a) { return a + 1;}

我用相同的参数调用了我的记忆方法两次:例如 addOne 和 5,第一次调用实际上应该调用 addOne 方法并返回结果,并为给定的参数存储该结果。当我第二次打电话时,它应该知道这已经被调用过,只需查找以前的答案。

我的想法是有一个类似HashMap<Callable,HashMap<List<Objects>,Object>> 的东西,您可以在其中存储以前的答案并稍后查找它们。我认为这可以通过 lambda 表达式以某种方式完成,但我对它们不太熟悉。我'不太确定如何编写此方法,希望能得到一些帮助。

用这种方法可以做到吗?

【问题讨论】:

  • 研究 Java 的代理机制。您可以创建拦截方法调用的对象的代理,存储返回值。如果您使用与先前调用相同的参数调用该方法,则无需调用底层方法即可获得相同的结果。 Spring 缓存为您完成了这项工作。

标签: java lambda memoization


【解决方案1】:

在 Java 8 中,您可以使用 ConcurrentHashMap.computeIfAbsent:

Map<Integer, Integer> cache = new ConcurrentHashMap<>();

Integer addOne(Integer x) {
    return cache.computeIfAbsent(x -> x + 1);
}

DZone 有 a good tutorial,它提供了适用于任何方法的解决方案:

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

【讨论】:

  • 虽然这是一个不错的 sn-p,但它没有回答 OP 的问题,即如何记忆 any 函数
  • @MrWiggles 您没有阅读链接,对吧?我编辑了我的帖子,使其更清晰。
  • 你是对的,我没有,谢谢你的澄清,我已经删除了 -1
  • computeIfAbsent(key, function::apply) 在功能上等同于仅将函数用作computeIfAbsent(key, function)。除了后者创建一个更少的 lambda 实例。
  • 第一个例子必须是Integer addOne(Integer n) { return cache.computeIfAbsent(n, x -&gt; x + 1); }
【解决方案2】:

如果您愿意放弃参数的类型安全,您可以使用 Java 8 的 MethodHandles 和 lambdas 来记忆任何函数:

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);
            }
        });
    };
}

Working Example

这将创建一个包含函数的可变参数 lambda,并且在构造 lambda 后几乎与直接调用函数一样快(即,在 call(Object...args) 内部没有发生反射),因为我们使用的是 MethodHandle.invoke() Method.invoke().

您仍然可以在没有 lambdas(替换为匿名类)和 MethodHandles(替换为 Method.invoke)的情况下执行此操作,但会有性能损失,这会降低对性能敏感的代码的吸引力。

【讨论】:

  • 不错的一个!是否有计划使用当前未使用的 returnType 参数?
  • 我认为它是用来验证 Method 的返回类型的,只是忘记了。
  • @DarthAndroid,返回类型不应该绑定为? extends V吗?如果方法实际上返回了 ? super V,那么它就不是真正的 args -&gt; V lambda,并且像这样转换返回值是不安全的。
  • 这是与泛型的权衡。如果我们不? super,那么V最多是一个原始类型(因为Class&lt;List&lt;String&gt;&gt;,例如,不幸的是在java中不是一个东西),所以调用者不能指定@的实际返回类型987654334@ 编译器可以理解的方式。我们所能做的就是将V 收紧到它的原始类型,但这仍然会失去泛型返回类型的类型安全性。无论您做什么,编译器都无法为泛型返回类型强制执行类型安全,如果我们不使用? super V 位,即使所有类型都是正确
猜你喜欢
  • 2014-05-03
  • 2017-07-07
  • 2018-10-29
  • 2021-06-16
  • 1970-01-01
  • 2014-03-23
  • 2012-01-03
  • 2011-04-25
  • 1970-01-01
相关资源
最近更新 更多