Memoizer.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.lang3.concurrent;

  18. import java.util.concurrent.CancellationException;
  19. import java.util.concurrent.ConcurrentHashMap;
  20. import java.util.concurrent.ConcurrentMap;
  21. import java.util.concurrent.ExecutionException;
  22. import java.util.concurrent.Future;
  23. import java.util.function.Function;

  24. import org.apache.commons.lang3.exception.ExceptionUtils;

  25. /**
  26.  * Definition of an interface for a wrapper around a calculation that takes a single parameter and returns a result. The
  27.  * results for the calculation will be cached for future requests.
  28.  *
  29.  * <p>
  30.  * This is not a fully functional cache, there is no way of limiting or removing results once they have been generated.
  31.  * However, it is possible to get the implementation to regenerate the result for a given parameter, if an error was
  32.  * thrown during the previous calculation, by setting the option during the construction of the class. If this is not
  33.  * set the class will return the cached exception.
  34.  * </p>
  35.  * <p>
  36.  * Thanks go to Brian Goetz, Tim Peierls and the members of JCP JSR-166 Expert Group for coming up with the
  37.  * original implementation of the class. It was also published within Java Concurrency in Practice as a sample.
  38.  * </p>
  39.  *
  40.  * @param <I> the type of the input to the calculation
  41.  * @param <O> the type of the output of the calculation
  42.  *
  43.  * @since 3.6
  44.  */
  45. public class Memoizer<I, O> implements Computable<I, O> {

  46.     private final ConcurrentMap<I, Future<O>> cache = new ConcurrentHashMap<>();
  47.     private final Function<? super I, ? extends Future<O>> mappingFunction;
  48.     private final boolean recalculate;

  49.     /**
  50.      * Constructs a Memoizer for the provided Computable calculation.
  51.      *
  52.      * <p>
  53.      * If a calculation throws an exception for any reason, this exception will be cached and returned for all future
  54.      * calls with the provided parameter.
  55.      * </p>
  56.      *
  57.      * @param computable the computation whose results should be memorized
  58.      */
  59.     public Memoizer(final Computable<I, O> computable) {
  60.         this(computable, false);
  61.     }

  62.     /**
  63.      * Constructs a Memoizer for the provided Computable calculation, with the option of whether a Computation that
  64.      * experiences an error should recalculate on subsequent calls or return the same cached exception.
  65.      *
  66.      * @param computable the computation whose results should be memorized
  67.      * @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call
  68.      *        failed
  69.      */
  70.     public Memoizer(final Computable<I, O> computable, final boolean recalculate) {
  71.         this.recalculate = recalculate;
  72.         this.mappingFunction = k -> FutureTasks.run(() -> computable.compute(k));
  73.     }

  74.     /**
  75.      * Constructs a Memoizer for the provided Function calculation.
  76.      *
  77.      * <p>
  78.      * If a calculation throws an exception for any reason, this exception will be cached and returned for all future
  79.      * calls with the provided parameter.
  80.      * </p>
  81.      *
  82.      * @param function the function whose results should be memorized
  83.      * @since 2.13.0
  84.      */
  85.     public Memoizer(final Function<I, O> function) {
  86.         this(function, false);
  87.     }

  88.     /**
  89.      * Constructs a Memoizer for the provided Function calculation, with the option of whether a Function that
  90.      * experiences an error should recalculate on subsequent calls or return the same cached exception.
  91.      *
  92.      * @param function the computation whose results should be memorized
  93.      * @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call
  94.      *        failed
  95.      * @since 2.13.0
  96.      */
  97.      public Memoizer(final Function<I, O> function, final boolean recalculate) {
  98.         this.recalculate = recalculate;
  99.         this.mappingFunction = k -> FutureTasks.run(() -> function.apply(k));
  100.     }

  101.     /**
  102.      * This method will return the result of the calculation and cache it, if it has not previously been calculated.
  103.      *
  104.      * <p>
  105.      * This cache will also cache exceptions that occur during the computation if the {@code recalculate} parameter in the
  106.      * constructor was set to {@code false}, or not set. Otherwise, if an exception happened on the previous calculation,
  107.      * the method will attempt again to generate a value.
  108.      * </p>
  109.      *
  110.      * @param arg the argument for the calculation
  111.      * @return the result of the calculation
  112.      * @throws InterruptedException thrown if the calculation is interrupted
  113.      */
  114.     @Override
  115.     public O compute(final I arg) throws InterruptedException {
  116.         while (true) {
  117.             final Future<O> future = cache.computeIfAbsent(arg, mappingFunction);
  118.             try {
  119.                 return future.get();
  120.             } catch (final CancellationException e) {
  121.                 cache.remove(arg, future);
  122.             } catch (final ExecutionException e) {
  123.                 if (recalculate) {
  124.                     cache.remove(arg, future);
  125.                 }
  126.                 throw launderException(e.getCause());
  127.             }
  128.         }
  129.     }

  130.     /**
  131.      * This method launders a Throwable to either a RuntimeException, Error or any other Exception wrapped in an
  132.      * IllegalStateException.
  133.      *
  134.      * @param throwable the throwable to laundered
  135.      * @return a RuntimeException, Error or an IllegalStateException
  136.      */
  137.     private RuntimeException launderException(final Throwable throwable) {
  138.         throw new IllegalStateException("Unchecked exception", ExceptionUtils.throwUnchecked(throwable));
  139.     }
  140. }