LANG-740: Implementation of a Memomizer (closes #203)
changes suggested in https://github.com/apache/commons-lang/pull/80: - tabs to spaces - use @Override - remove unused variables - other minimal clean-ups
This commit is contained in:
parent
c0c7112dcd
commit
9f89fd4626
|
@ -55,6 +55,7 @@ The <action> type attribute can be add,update,fix,remove.
|
|||
<action issue="LANG-1070" type="fix" dev="pschumacher" due-to="Paul Pogonyshev">ArrayUtils#add confusing example in javadoc</action>
|
||||
<action issue="LANG-1271" type="fix" dev="pschumacher" due-to="Pierre Templier">StringUtils#isAnyEmpty and #isAnyBlank should return false for an empty array</action>
|
||||
<action issue="LANG-1155" type="fix" dev="pschumacher" due-to="Saif Asif, Thiago Andrade">Add StringUtils#unwrap</action>
|
||||
<action issue="LANG-740" type="add" dev="pschumacher" due-to="James Sawle">Implementation of a Memomizer</action>
|
||||
<action issue="LANG-1258" type="add" dev="pschumacher" due-to="IG, Grzegorz Rożniecki">Add ArrayUtils#toStringArray method</action>
|
||||
<action issue="LANG-1160" type="add" dev="kinow">StringUtils#abbreviate should support 'custom ellipses' parameter</action>
|
||||
<action issue="LANG-1270" type="add" dev="pschumacher" due-to="Pierre Templier">Add StringUtils#isAnyNotEmpty and #isAnyNotBlank</action>
|
||||
|
|
|
@ -21,17 +21,21 @@ package org.apache.commons.lang3.concurrent;
|
|||
* <p/>
|
||||
* <p>This interface allows for wrapping a calculation into a class so that it maybe passed around an application.</p>
|
||||
*
|
||||
* @param <A> the type of the input to the calculation
|
||||
* @param <V> the type of the output of the calculation
|
||||
* @param <I> the type of the input to the calculation
|
||||
* @param <O> the type of the output of the calculation
|
||||
*
|
||||
* @since 3.6
|
||||
*/
|
||||
public interface Computable<A, V> {
|
||||
public interface Computable<I, O> {
|
||||
|
||||
/**
|
||||
* This method carries out the given operation with the provided argument.
|
||||
*
|
||||
* @param arg the argument for the calculation
|
||||
* @return the result of the calculation
|
||||
* @throws InterruptedException thrown if the calculation is interrupted
|
||||
*/
|
||||
V compute(final A arg) throws InterruptedException;
|
||||
/**
|
||||
* This method carries out the given operation with the provided argument.
|
||||
*
|
||||
* @param arg
|
||||
* the argument for the calculation
|
||||
* @return the result of the calculation
|
||||
* @throws InterruptedException
|
||||
* thrown if the calculation is interrupted
|
||||
*/
|
||||
O compute(final I arg) throws InterruptedException;
|
||||
}
|
||||
|
|
|
@ -25,109 +25,141 @@ import java.util.concurrent.Future;
|
|||
import java.util.concurrent.FutureTask;
|
||||
|
||||
/**
|
||||
* <p>Definition of an interface for a wrapper around a calculation that takes a single parameter and returns a result.
|
||||
* The results for the calculation will be cached for future requests.</p>
|
||||
* <p/>
|
||||
* <p>This is not a fully functional cache, there is no way of limiting or removing results once they have been generated.
|
||||
* However, it is possible to get the implementation to regenerate the result for a given parameter, if an error was
|
||||
* thrown during the previous calculation, by setting the option during the construction of the class. If this is not
|
||||
* set the class will return the cached exception.</p>
|
||||
* <p/>
|
||||
* <p>Thanks should go to Brian Goetz, Tim Peierls and the members of JCP JSR-166 Expert Group for coming up with the
|
||||
* original implementation of the class. It was also published within Java Concurreny in Practice as a sample.</p>
|
||||
* <p>
|
||||
* Definition of an interface for a wrapper around a calculation that takes a
|
||||
* single parameter and returns a result. The results for the calculation will
|
||||
* be cached for future requests.
|
||||
* </p>
|
||||
* <p>
|
||||
* This is not a fully functional cache, there is no way of limiting or removing
|
||||
* results once they have been generated. However, it is possible to get the
|
||||
* implementation to regenerate the result for a given parameter, if an error
|
||||
* was thrown during the previous calculation, by setting the option during the
|
||||
* construction of the class. If this is not set the class will return the
|
||||
* cached exception.
|
||||
* </p>
|
||||
* <p>
|
||||
* Thanks should go to Brian Goetz, Tim Peierls and the members of JCP JSR-166
|
||||
* Expert Group for coming up with the original implementation of the class. It
|
||||
* was also published within Java Concurrency in Practice as a sample.
|
||||
* </p>
|
||||
*
|
||||
* @param <A> the type of the input to the calculation
|
||||
* @param <V> the type of the output of the calculation
|
||||
* @param <I>
|
||||
* the type of the input to the calculation
|
||||
* @param <O>
|
||||
* the type of the output of the calculation
|
||||
*
|
||||
* @since 3.6
|
||||
*/
|
||||
public class Memoizer<A, V> implements Computable<A, V> {
|
||||
private final ConcurrentMap<A, Future<V>> cache
|
||||
= new ConcurrentHashMap<A, Future<V>>();
|
||||
private final Computable<A, V> c;
|
||||
private final boolean recalculate;
|
||||
public class Memoizer<I, O> implements Computable<I, O> {
|
||||
|
||||
/**
|
||||
* <p>Constructs a Memoizer for the provided Computable calculation.</p>
|
||||
* <p/>
|
||||
* <p>If a calculation is thrown an exception for any reason, this exception will be cached and returned for
|
||||
* all future calls with the provided parameter.</p>
|
||||
*
|
||||
* @param c the computation whose results should be memorized
|
||||
*/
|
||||
public Memoizer(Computable<A, V> c) {
|
||||
this(c, false);
|
||||
}
|
||||
private final ConcurrentMap<I, Future<O>> cache = new ConcurrentHashMap<>();
|
||||
private final Computable<I, O> computable;
|
||||
private final boolean recalculate;
|
||||
|
||||
/**
|
||||
* <p>Constructs a Memoizer for the provided Computable calculation, with the option of whether a Computation
|
||||
* that experiences an error should recalculate on subsequent calls or return the same cached exception.</p>
|
||||
*
|
||||
* @param c the computation whose results should be memorized
|
||||
* @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous
|
||||
* call failed
|
||||
*/
|
||||
public Memoizer(Computable<A, V> c, boolean recalculate) {
|
||||
this.c = c;
|
||||
this.recalculate = recalculate;
|
||||
}
|
||||
/**
|
||||
* <p>
|
||||
* Constructs a Memoizer for the provided Computable calculation.
|
||||
* </p>
|
||||
* <p>
|
||||
* If a calculation is thrown an exception for any reason, this exception
|
||||
* will be cached and returned for all future calls with the provided
|
||||
* parameter.
|
||||
* </p>
|
||||
*
|
||||
* @param computable
|
||||
* the computation whose results should be memorized
|
||||
*/
|
||||
public Memoizer(final Computable<I, O> computable) {
|
||||
this(computable, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>This method will return the result of the calculation and cache it, if it has not previously been calculated.</p>
|
||||
* <p/>
|
||||
* <p>This cache will also cache exceptions that occur during the computation if the {@code recalculate} parameter is
|
||||
* the constructor was set to {@code false}, or not set. Otherwise, if an exception happened on the previous
|
||||
* calculation, the method will attempt again to generate a value.</p>
|
||||
*
|
||||
* @param arg the argument for the calculation
|
||||
* @return the result of the calculation
|
||||
* @throws InterruptedException thrown if the calculation is interrupted
|
||||
* @throws IllegalStateException a wrapper around any checked exception that occurs during the computation of the result
|
||||
*/
|
||||
public V compute(final A arg) throws InterruptedException, IllegalStateException {
|
||||
while (true) {
|
||||
Future<V> f = cache.get(arg);
|
||||
if (f == null) {
|
||||
Callable<V> eval = new Callable<V>() {
|
||||
public V call() throws InterruptedException {
|
||||
return c.compute(arg);
|
||||
}
|
||||
};
|
||||
FutureTask<V> ft = new FutureTask<V>(eval);
|
||||
f = cache.putIfAbsent(arg, ft);
|
||||
if (f == null) {
|
||||
f = ft;
|
||||
ft.run();
|
||||
}
|
||||
}
|
||||
try {
|
||||
return f.get();
|
||||
}
|
||||
catch (CancellationException e) {
|
||||
cache.remove(arg, f);
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
if (recalculate) {
|
||||
cache.remove(arg, f);
|
||||
}
|
||||
/**
|
||||
* <p>
|
||||
* Constructs a Memoizer for the provided Computable calculation, with the
|
||||
* option of whether a Computation that experiences an error should
|
||||
* recalculate on subsequent calls or return the same cached exception.
|
||||
* </p>
|
||||
*
|
||||
* @param computable
|
||||
* the computation whose results should be memorized
|
||||
* @param recalculate
|
||||
* determines whether the computation should be recalculated on
|
||||
* subsequent calls if the previous call failed
|
||||
*/
|
||||
public Memoizer(final Computable<I, O> computable, final boolean recalculate) {
|
||||
this.computable = computable;
|
||||
this.recalculate = recalculate;
|
||||
}
|
||||
|
||||
throw launderException(e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* <p>
|
||||
* This method will return the result of the calculation and cache it, if it
|
||||
* has not previously been calculated.
|
||||
* </p>
|
||||
* <p>
|
||||
* This cache will also cache exceptions that occur during the computation
|
||||
* if the {@code recalculate} parameter is the constructor was set to
|
||||
* {@code false}, or not set. Otherwise, if an exception happened on the
|
||||
* previous calculation, the method will attempt again to generate a value.
|
||||
* </p>
|
||||
*
|
||||
* @param arg
|
||||
* the argument for the calculation
|
||||
* @return the result of the calculation
|
||||
* @throws InterruptedException
|
||||
* thrown if the calculation is interrupted
|
||||
*/
|
||||
@Override
|
||||
public O compute(final I arg) throws InterruptedException {
|
||||
while (true) {
|
||||
Future<O> future = cache.get(arg);
|
||||
if (future == null) {
|
||||
Callable<O> eval = new Callable<O>() {
|
||||
|
||||
/**
|
||||
* <p>This method launders a Throwable to either a RuntimeException, Error or any other Exception wrapped
|
||||
* in an IllegalStateException.</p>
|
||||
*
|
||||
* @param t the throwable to laundered
|
||||
* @return a RuntimeException, Error or an IllegalStateException
|
||||
*/
|
||||
private RuntimeException launderException(Throwable t) {
|
||||
if (t instanceof RuntimeException) {
|
||||
return (RuntimeException) t;
|
||||
} else if (t instanceof Error) {
|
||||
throw (Error) t;
|
||||
} else {
|
||||
throw new IllegalStateException("Unchecked exception", t);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public O call() throws InterruptedException {
|
||||
return computable.compute(arg);
|
||||
}
|
||||
};
|
||||
FutureTask<O> futureTask = new FutureTask<>(eval);
|
||||
future = cache.putIfAbsent(arg, futureTask);
|
||||
if (future == null) {
|
||||
future = futureTask;
|
||||
futureTask.run();
|
||||
}
|
||||
}
|
||||
try {
|
||||
return future.get();
|
||||
} catch (CancellationException e) {
|
||||
cache.remove(arg, future);
|
||||
} catch (ExecutionException e) {
|
||||
if (recalculate) {
|
||||
cache.remove(arg, future);
|
||||
}
|
||||
|
||||
throw launderException(e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* This method launders a Throwable to either a RuntimeException, Error or
|
||||
* any other Exception wrapped in an IllegalStateException.
|
||||
* </p>
|
||||
*
|
||||
* @param throwable
|
||||
* the throwable to laundered
|
||||
* @return a RuntimeException, Error or an IllegalStateException
|
||||
*/
|
||||
private RuntimeException launderException(final Throwable throwable) {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return (RuntimeException) throwable;
|
||||
} else if (throwable instanceof Error) {
|
||||
throw (Error) throwable;
|
||||
} else {
|
||||
throw new IllegalStateException("Unchecked exception", throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.commons.lang3.concurrent;
|
||||
|
||||
import org.easymock.EasyMockRunner;
|
||||
|
@ -13,100 +29,94 @@ import static org.junit.Assert.fail;
|
|||
@RunWith(EasyMockRunner.class)
|
||||
public class MemoizerTest {
|
||||
|
||||
@Mock
|
||||
private Computable<Integer, Integer> computable;
|
||||
@Mock
|
||||
private Computable<Integer, Integer> computable;
|
||||
|
||||
@Test
|
||||
public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
|
||||
expect(computable.compute(input)).andReturn(input);
|
||||
replay(computable);
|
||||
@Test
|
||||
public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
|
||||
expect(computable.compute(input)).andReturn(input);
|
||||
replay(computable);
|
||||
|
||||
assertEquals("Should call computable first time", input, memoizer.compute(input));
|
||||
assertEquals("Should not call the computable the second time", input, memoizer.compute(input));
|
||||
}
|
||||
assertEquals("Should call computable first time", input, memoizer.compute(input));
|
||||
assertEquals("Should not call the computable the second time", input, memoizer.compute(input));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
|
||||
Integer input = 1;
|
||||
Integer answer = 3;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
|
||||
InterruptedException interruptedException = new InterruptedException();
|
||||
expect(computable.compute(input)).andThrow(interruptedException);
|
||||
replay(computable);
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
|
||||
InterruptedException interruptedException = new InterruptedException();
|
||||
expect(computable.compute(input)).andThrow(interruptedException);
|
||||
replay(computable);
|
||||
|
||||
try {
|
||||
memoizer.compute(input);
|
||||
fail();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
//Should always be thrown the first time
|
||||
}
|
||||
try {
|
||||
memoizer.compute(input);
|
||||
fail("Expected Throwable to be thrown!");
|
||||
} catch (Throwable expected) {
|
||||
// Should always be thrown the first time
|
||||
}
|
||||
|
||||
memoizer.compute(input);
|
||||
}
|
||||
memoizer.compute(input);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
|
||||
Integer input = 1;
|
||||
Integer answer = 3;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable, false);
|
||||
InterruptedException interruptedException = new InterruptedException();
|
||||
expect(computable.compute(input)).andThrow(interruptedException);
|
||||
replay(computable);
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable, false);
|
||||
InterruptedException interruptedException = new InterruptedException();
|
||||
expect(computable.compute(input)).andThrow(interruptedException);
|
||||
replay(computable);
|
||||
|
||||
try {
|
||||
memoizer.compute(input);
|
||||
fail();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
//Should always be thrown the first time
|
||||
}
|
||||
try {
|
||||
memoizer.compute(input);
|
||||
fail("Expected Throwable to be thrown!");
|
||||
} catch (Throwable expected) {
|
||||
// Should always be thrown the first time
|
||||
}
|
||||
|
||||
memoizer.compute(input);
|
||||
}
|
||||
memoizer.compute(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesRecalculateWhenSetToTrue() throws Exception {
|
||||
Integer input = 1;
|
||||
Integer answer = 3;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable, true);
|
||||
InterruptedException interruptedException = new InterruptedException();
|
||||
expect(computable.compute(input)).andThrow(interruptedException).andReturn(answer);
|
||||
replay(computable);
|
||||
@Test
|
||||
public void testDoesRecalculateWhenSetToTrue() throws Exception {
|
||||
Integer input = 1;
|
||||
Integer answer = 3;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable, true);
|
||||
InterruptedException interruptedException = new InterruptedException();
|
||||
expect(computable.compute(input)).andThrow(interruptedException).andReturn(answer);
|
||||
replay(computable);
|
||||
|
||||
try {
|
||||
memoizer.compute(input);
|
||||
fail();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
//Should always be thrown the first time
|
||||
}
|
||||
try {
|
||||
memoizer.compute(input);
|
||||
fail("Expected Throwable to be thrown!");
|
||||
} catch (Throwable expected) {
|
||||
// Should always be thrown the first time
|
||||
}
|
||||
|
||||
assertEquals(answer, memoizer.compute(input));
|
||||
}
|
||||
assertEquals(answer, memoizer.compute(input));
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testWhenComputableThrowsRuntimeException() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
|
||||
RuntimeException runtimeException = new RuntimeException("Some runtime exception");
|
||||
expect(computable.compute(input)).andThrow(runtimeException);
|
||||
replay(computable);
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testWhenComputableThrowsRuntimeException() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
|
||||
RuntimeException runtimeException = new RuntimeException("Some runtime exception");
|
||||
expect(computable.compute(input)).andThrow(runtimeException);
|
||||
replay(computable);
|
||||
memoizer.compute(input);
|
||||
}
|
||||
|
||||
memoizer.compute(input);
|
||||
}
|
||||
@Test(expected = Error.class)
|
||||
public void testWhenComputableThrowsError() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
|
||||
Error error = new Error();
|
||||
expect(computable.compute(input)).andThrow(error);
|
||||
replay(computable);
|
||||
|
||||
@Test(expected = Error.class)
|
||||
public void testWhenComputableThrowsError() throws Exception {
|
||||
Integer input = 1;
|
||||
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
|
||||
Error error = new Error();
|
||||
expect(computable.compute(input)).andThrow(error);
|
||||
replay(computable);
|
||||
|
||||
memoizer.compute(input);
|
||||
}
|
||||
memoizer.compute(input);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue