Java общая лямбда-мемоизация - PullRequest
3 голосов
/ 07 мая 2020

Я хочу написать утилиту для общей мемоизации на Java, я хочу, чтобы код мог выглядеть так:

Util.memoize(() -> longCalculation(1));

где

private Integer longCalculation(Integer x) {
    try { 
        Thread.sleep(1000);
    } catch (InterruptedException ignored) {}
    return x * 2;
}

Для этого , Я думал, что могу сделать что-то вроде этого:

public class Util{
    private static final Map<Object, Object> cache = new ConcurrentHashMap<>();
    public interface Operator<T> {
        T op();
    }

    public static<T> T memoize(Operator<T> o) {
        ConcurrentHashMap<Object, T> memo = cache.containsKey(o.getClass()) ? (ConcurrentHashMap<Object, T>) cache.get(o.getClass()) : new ConcurrentHashMap<>();
        if (memo.containsKey(o)) {
            return memo.get(o);
        } else {
            T val = o.op();
            memo.put(o, val);
            return val;
        }
    }
}

Я ожидал, что это сработает, но я не вижу, чтобы мемоизация выполнялась. Я отследил это до того, что o.getClass() отличается для каждого вызова. Я думал, что могу попробовать получить тип времени выполнения T, но не могу придумать, как это сделать.

Ответы [ 2 ]

4 голосов
/ 07 мая 2020

Ответ от Lino указывает на пару fl aws в коде, но не работает, если не использовать повторно ту же лямбду.

Это потому, что o.getClass() возвращает не класс того, что возвращает лямбда, а класс самой лямбды. Таким образом, приведенный ниже код возвращает два разных класса:

Util.memoize(() -> longCalculation(1));
Util.memoize(() -> longCalculation(1));

Я не думаю, что есть хороший способ узнать класс возвращаемого типа без фактического выполнения потенциально длительного кода, который из конечно - это то, чего вы хотите избежать.

Имея это в виду, я бы предложил передать класс в качестве второго параметра в memoize(). Это даст вам:

@SuppressWarnings("unchecked")
public static <T> T memoize(Operator<T> o, Class<T> clazz) {
  return (T) cache.computeIfAbsent(clazz, k -> o.op());
}

Это основано на том, что вы измените тип cache на:

private static final Map<Class<?>, Object> cache = new ConcurrentHashMap<>();

К сожалению, вам нужно уменьшить Object до T, но вы можете гарантировать безопасность с аннотацией @SuppressWarnings("unchecked"). В конце концов, вы контролируете код и знаете, что класс значения будет таким же, как и ключ на карте.

Альтернативой было бы использование Guavas ClassToInstanceMap:

private static final ClassToInstanceMap<Object> cache = MutableClassToInstanceMap.create(new ConcurrentHashMap<>());

Это, однако, не позволяет вам использовать computeIfAbsent() без приведения, так как он возвращает Object, поэтому код станет немного более подробным:

public static <T> T memoize(Operator<T> o, Class<T> clazz) {
  T cachedCalculation = cache.getInstance(clazz);
  if (cachedCalculation != null) {
    return cachedCalculation;
  }
  T calculation = o.op();
  cache.put(clazz, calculation);
  return calculation;
}

В качестве последнего примечания, вам не нужно указывать свой собственный функциональный интерфейс, но вы можете использовать интерфейс Supplier:

@SuppressWarnings("unchecked")
public static <T> T memoize(Supplier<T> o, Class<T> clazz) {
  return (T) cache.computeIfAbsent(clazz, k -> o.get());
}
2 голосов
/ 07 мая 2020

Проблема у вас в строке:

ConcurrentHashMap<Object, T> memo = cache.containsKey(o.getClass()) ? (ConcurrentHashMap<Object, T>) cache.get(o.getClass()) : new ConcurrentHashMap<>();

Вы проверяете, существует ли запись с ключом o.getClass(). Если да, вы get(), иначе вы используете недавно инициализированный ConcurrentHashMap. Проблема в том, что вы не сохраняете эту вновь созданную карту обратно в кеш.

Так что либо:

  • Поместите cache.put(o.getClass(), memo); после строки выше
  • Или, что еще лучше, используйте метод computeIfAbsent():

    ConcurrentHashMap<Object, T> memo = cache.computeIfAbsent(o.getClass(), 
                                                              k -> new ConcurrentHashMap<>());
    

Также, поскольку вы знаете структуру своего cache, вы можете сделать его более безопасным, так что вам не нужно использовать везде:

private static final Map<Object, Map<Operator<?>, Object>> cache = new ConcurrentHashMap<>();

Также вы можете еще больше сократить свой метод, используя ранее упомянутый computeIfAbsent():

public static <T> T memoize(Operator<T> o) {
    return (T) cache
        .computeIfAbsent(o.getClass(), k -> new ConcurrentHashMap<>())
        .computeIfAbsent(o, k -> o.op());
}
  1. (T): просто преобразует неизвестный тип возврата Object в требуемый тип вывода T
  2. .computeIfAbsent(o.getClass(), k -> new ConcurrentHashMap<>()): вызывает предоставленную лямбду k -> new ConcurrentHashMap<>(), когда нет сопоставления для ключа o.getClass() в cache
  3. .computeIfAbsent(o, k -> o.op());: это вызывается для возвращаемого значения из computeIfAbsent вызова 2. . Если o не существует во вложенной карте, выполните лямбда k -> o.op(), возвращаемое значение затем сохраняется в карте и возвращается.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...