Introduction à Guava Memoizer

Introduction à Guava Memoizer

1. Vue d'ensemble

Dans ce didacticiel, nous allons explorer les fonctionnalités de mémoization de la bibliothèque de Googles.

La mémorisation est une technique qui évite l'exécution répétée d'une fonction coûteuse en mettant en cache le résultat de la première exécution de la fonction.

1.1. Memoization vs. Mise en cache

La mémorisation est similaire à la mise en cache en ce qui concerne le stockage en mémoire. Les deux techniques tentent deincrease efficiency by reducing the number of calls to computationally expensive code.

Cependant, alors que la mise en cache est un terme plus générique qui résout le problème au niveau de l'instanciation de classe, de la récupération d'objets ou de la récupération de contenu,memoization solves the problem at the level of method/function execution.

1.2. Goyave Memoizer et Guava Cache

Guava prend en charge la mémoisation et la mise en cache. Memoization applies to functions with no argument (Supplier) and functions with exactly one argument (Function).Supplier etFunction ici font référence aux interfaces fonctionnelles Guava qui sont des sous-classes directes des interfaces API fonctionnelles Java 8 du même nom.

Depuis la version 23.6, Guava ne prend pas en charge la mémorisation de fonctions avec plus d'un argument.

Nous pouvons appeler des API de mémoization à la demande et spécifier une stratégie d'éviction qui contrôle le nombre d'entrées en mémoire et empêche la croissance incontrôlée de la mémoire utilisée en supprimant / supprimant une entrée du cache une fois qu'elle correspond à la condition de la stratégie.

La mémorisation utilise le cache de goyave; pour plus d'informations sur Guava Cache, veuillez consulter nosGuava Cache article.

2. MémorisationSupplier

Il existe deux méthodes dans la classeSuppliers qui permettent la mémorisation:memoize etmemoizeWithExpiration.

Lorsque nous voulons exécuter la méthode mémorisée, nous pouvons simplement appeler la méthodeget desSupplier retournés. Depending on whether the method’s return value exists in memory, the get method will either return the in-memory value or execute the memoized method and pass the return value to the caller.

Explorons chaque méthode de mémorisation deSupplier.

2.1. MémorisationSupplier sans expulsion

Nous pouvons utiliser la méthodeSuppliersmemoize et spécifier lesSupplier délégués comme référence de méthode:

Supplier memoizedSupplier = Suppliers.memoize(
  CostlySupplier::generateBigNumber);

Comme nous n'avons pas spécifié de politique d'expulsion,once the get method is called, the returned value will persist in memory while the Java application is still running. Tous les appels àget après l'appel initial renverront la valeur mémorisée.

2.2. MémorisationSupplier avec expulsion par Time-To-Live (TTL)

Supposons que nous souhaitons uniquement conserver la valeur renvoyée par lesSupplier dans le mémo pendant une certaine période.

Nous pouvons utiliser la méthodeSuppliers 'memoizeWithExpiration et spécifier le temps d'expiration avec son unité de temps correspondante (par exemple, seconde, minute), en plus desSupplier délégués:

Supplier memoizedSupplier = Suppliers.memoizeWithExpiration(
  CostlySupplier::generateBigNumber, 5, TimeUnit.SECONDS);

After the specified time has passed (5 seconds), the cache will evict the returned value of the Supplier from memory et tout appel ultérieur à la méthodeget réexécutentgenerateBigNumber.

Pour plus d'informations, reportez-vous auxJavadoc.

2.3. Exemple

Simulons une méthode coûteuse en calcul nomméegenerateBigNumber:

public class CostlySupplier {
    private static BigInteger generateBigNumber() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {}
        return new BigInteger("12345");
    }
}

Notre exemple de méthode prendra 2 secondes pour s'exécuter, puis retournera un résultatBigInteger. Nous pourrions le mémoriser en utilisant les APImemoize oumemoizeWithExpiration.

Pour plus de simplicité, nous omettons la politique d'expulsion:

@Test
public void givenMemoizedSupplier_whenGet_thenSubsequentGetsAreFast() {
    Supplier memoizedSupplier;
    memoizedSupplier = Suppliers.memoize(CostlySupplier::generateBigNumber);

    BigInteger expectedValue = new BigInteger("12345");
    assertSupplierGetExecutionResultAndDuration(
      memoizedSupplier, expectedValue, 2000D);
    assertSupplierGetExecutionResultAndDuration(
      memoizedSupplier, expectedValue, 0D);
    assertSupplierGetExecutionResultAndDuration(
      memoizedSupplier, expectedValue, 0D);
}

private  void assertSupplierGetExecutionResultAndDuration(
  Supplier 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)));
}

Le premier appel de la méthodeget prend deux secondes, comme simulé dans la méthodegenerateBigNumber; cependant,subsequent calls to get() will execute significantly faster, since the generateBigNumber result has been memoized.

3. MémorisationFunction

Pour mémoriser une méthode qui prend un seul argument, nousbuild a LoadingCache map using CacheLoader‘s from method to provision the builder concerning our method as a Guava Function.

LoadingCache est une mappe simultanée, avec des valeurs chargées automatiquement parCacheLoader. CacheLoader populates the map by computing the Function specified in the from method, et mettre la valeur retournée dans lesLoadingCache. Pour plus d'informations, reportez-vous auxJavadoc.

La clé deLoadingCache est l'argument / l'entrée deFunction, tandis que la valeur de la carte est la valeur renvoyée parFunction:

LoadingCache memo = CacheBuilder.newBuilder()
  .build(CacheLoader.from(FibonacciSequence::getFibonacciNumber));

PuisqueLoadingCache est une mappe simultanée,it doesn’t allow null keys or values. Par conséquent, nous devons nous assurer queFunction ne prend pas en charge null comme argument ou ne renvoie pas de valeurs nulles.

3.1. Mémorisation deFunction avec les politiques d'expulsion

Nous pouvons appliquer une politique d'éviction différente de Guava Cache lorsque nous mémorisons unFunction comme mentionné dans la section 3 desGuava Cache article.

Par exemple, nous pouvons supprimer les entrées inactives pendant 2 secondes:

LoadingCache memo = CacheBuilder.newBuilder()
  .expireAfterAccess(2, TimeUnit.SECONDS)
  .build(CacheLoader.from(Fibonacci::getFibonacciNumber));

Examinons ensuite deux cas d’utilisation de la mémorisation deFunction: la séquence de Fibonacci et la factorielle.

3.2. Exemple de séquence de Fibonacci

On peut calculer récursivement un nombre de Fibonacci à partir d'un nombre donnén:

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

Sans mémorisation, lorsque la valeur d'entrée est relativement élevée, l'exécution de la fonction sera lente.

Pour améliorer l'efficacité et les performances, nous pouvons mémorisergetFibonacciNumber à l'aide deCacheLoader etCacheBuilder, en spécifiant la politique d'éviction si nécessaire.

Dans l'exemple suivant, nous supprimons l'entrée la plus ancienne une fois que la taille du mémo a atteint 100 entrées:

public class FibonacciSequence {
    private static LoadingCache memo = CacheBuilder.newBuilder()
      .maximumSize(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 {
            return memo.getUnchecked(n - 1).add(memo.getUnchecked(n - 2));
        }
    }
}

Ici, nous utilisons la méthodegetUnchecked qui renvoie la valeur si elle existe sans lever d'exception vérifiée.

Dans ce cas, nous n'avons pas besoin de gérer explicitement l'exception lors de la spécification de la référence de méthodegetFibonacciNumber dans l'appel de méthodefrom deCacheLoader.

Pour plus d'informations, reportez-vous auxJavadoc.

3.3. Exemple factoriel

Ensuite, nous avons une autre méthode récursive qui calcule la factorielle d'une valeur d'entrée donnée, n:

public static BigInteger getFactorial(int n) {
    if (n == 0) {
        return BigInteger.ONE;
    } else {
        return BigInteger.valueOf(n).multiply(getFactorial(n - 1));
    }
}

Nous pouvons améliorer l'efficacité de cette implémentation en appliquant la mémorisation:

public class Factorial {
    private static LoadingCache 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. Conclusion

Dans cet article, nous avons vu comment Guava fournit des API pour effectuer la mémorisation des méthodesSupplier etFunction. Nous avons également montré comment spécifier en mémoire la politique d’expulsion du résultat de la fonction stockée.

Comme toujours, le code source peut être trouvéover on GitHub.