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:
pascalschumacher 2016-10-28 21:55:22 +02:00
parent c0c7112dcd
commit 9f89fd4626
4 changed files with 237 additions and 190 deletions

View File

@ -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>

View File

@ -21,17 +21,21 @@
* <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
* @param arg
* the argument for the calculation
* @return the result of the calculation
* @throws InterruptedException thrown if the calculation is interrupted
* @throws InterruptedException
* thrown if the calculation is interrupted
*/
V compute(final A arg) throws InterruptedException;
O compute(final I arg) throws InterruptedException;
}

View File

@ -25,88 +25,117 @@
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;
public class Memoizer<I, O> implements Computable<I, O> {
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.</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>
* <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 c the computation whose results should be memorized
* @param computable
* the computation whose results should be memorized
*/
public Memoizer(Computable<A, V> c) {
this(c, false);
public Memoizer(final Computable<I, O> computable) {
this(computable, false);
}
/**
* <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>
* <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
* @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(Computable<A, V> c, boolean recalculate) {
this.c = c;
public Memoizer(final Computable<I, O> computable, final boolean recalculate) {
this.computable = computable;
this.recalculate = recalculate;
}
/**
* <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>
* <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
* @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
* @throws InterruptedException
* thrown if the calculation is interrupted
*/
public V compute(final A arg) throws InterruptedException, IllegalStateException {
@Override
public O compute(final I arg) throws InterruptedException {
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);
Future<O> future = cache.get(arg);
if (future == null) {
Callable<O> eval = new Callable<O>() {
@Override
public O call() throws InterruptedException {
return computable.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
FutureTask<O> futureTask = new FutureTask<>(eval);
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
futureTask.run();
}
}
try {
return f.get();
}
catch (CancellationException e) {
cache.remove(arg, f);
}
catch (ExecutionException e) {
return future.get();
} catch (CancellationException e) {
cache.remove(arg, future);
} catch (ExecutionException e) {
if (recalculate) {
cache.remove(arg, f);
cache.remove(arg, future);
}
throw launderException(e.getCause());
@ -115,19 +144,22 @@ public V call() throws InterruptedException {
}
/**
* <p>This method launders a Throwable to either a RuntimeException, Error or any other Exception wrapped
* in an IllegalStateException.</p>
* <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
* @param throwable
* 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;
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", t);
throw new IllegalStateException("Unchecked exception", throwable);
}
}
}

View File

@ -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;
@ -19,7 +35,7 @@ public class MemoizerTest {
@Test
public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception {
Integer input = 1;
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
expect(computable.compute(input)).andReturn(input);
replay(computable);
@ -30,18 +46,16 @@ public void testOnlyCallComputableOnceIfDoesNotThrowException() throws Exception
@Test(expected = IllegalStateException.class)
public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exception {
Integer input = 1;
Integer answer = 3;
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
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
fail("Expected Throwable to be thrown!");
} catch (Throwable expected) {
// Should always be thrown the first time
}
memoizer.compute(input);
@ -50,18 +64,16 @@ public void testDefaultBehaviourNotToRecalculateExecutionExceptions() throws Exc
@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);
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
fail("Expected Throwable to be thrown!");
} catch (Throwable expected) {
// Should always be thrown the first time
}
memoizer.compute(input);
@ -71,27 +83,25 @@ public void testDoesNotRecalculateWhenSetToFalse() throws Exception {
public void testDoesRecalculateWhenSetToTrue() throws Exception {
Integer input = 1;
Integer answer = 3;
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable, true);
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
fail("Expected Throwable to be thrown!");
} catch (Throwable expected) {
// Should always be thrown the first time
}
assertEquals(answer, memoizer.compute(input));
}
@Test(expected = RuntimeException.class)
public void testWhenComputableThrowsRuntimeException() throws Exception {
Integer input = 1;
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
RuntimeException runtimeException = new RuntimeException("Some runtime exception");
expect(computable.compute(input)).andThrow(runtimeException);
replay(computable);
@ -102,7 +112,7 @@ public void testWhenComputableThrowsRuntimeException() throws Exception {
@Test(expected = Error.class)
public void testWhenComputableThrowsError() throws Exception {
Integer input = 1;
Memoizer<Integer, Integer> memoizer = new Memoizer<Integer, Integer>(computable);
Memoizer<Integer, Integer> memoizer = new Memoizer<>(computable);
Error error = new Error();
expect(computable.compute(input)).andThrow(error);
replay(computable);