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
Project: http://git-wip-us.apache.org/repos/asf/commons-lang/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-lang/commit/9f89fd46 Tree: http://git-wip-us.apache.org/repos/asf/commons-lang/tree/9f89fd46 Diff: http://git-wip-us.apache.org/repos/asf/commons-lang/diff/9f89fd46 Branch: refs/heads/master Commit: 9f89fd4626bbb4e34a905835e397bcffbad59307 Parents: c0c7112 Author: pascalschumacher <pascalschumac...@gmx.net> Authored: Fri Oct 28 21:55:22 2016 +0200 Committer: pascalschumacher <pascalschumac...@gmx.net> Committed: Sun Nov 13 17:51:00 2016 +0100 ---------------------------------------------------------------------- src/changes/changes.xml | 1 + .../commons/lang3/concurrent/Computable.java | 26 ++- .../commons/lang3/concurrent/Memoizer.java | 228 +++++++++++-------- .../commons/lang3/concurrent/MemoizerTest.java | 202 ++++++++-------- 4 files changed, 252 insertions(+), 205 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/changes/changes.xml ---------------------------------------------------------------------- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d0d86fc..ecfa481 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -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> http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/main/java/org/apache/commons/lang3/concurrent/Computable.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/lang3/concurrent/Computable.java b/src/main/java/org/apache/commons/lang3/concurrent/Computable.java index 7934330..d9e5468 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/Computable.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/Computable.java @@ -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; } http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java b/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java index 5682b0d..d0fe406 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/Memoizer.java @@ -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); + } + } } http://git-wip-us.apache.org/repos/asf/commons-lang/blob/9f89fd46/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java index aa765f8..ae511c9 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/MemoizerTest.java @@ -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; - - @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); - - 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); - - try { - memoizer.compute(input); - fail(); - } - catch (Throwable ex) { - //Should always be thrown the first time - } - - 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); - - try { - memoizer.compute(input); - fail(); - } - catch (Throwable ex) { - //Should always be thrown the first time - } - - 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); - - try { - memoizer.compute(input); - fail(); - } - catch (Throwable ex) { - //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); - RuntimeException runtimeException = new RuntimeException("Some runtime exception"); - expect(computable.compute(input)).andThrow(runtimeException); - replay(computable); - - memoizer.compute(input); - } - - @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); - } + @Mock + private Computable<Integer, Integer> 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)); + } + + @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("Expected Throwable to be thrown!"); + } catch (Throwable expected) { + // Should always be thrown the first time + } + + memoizer.compute(input); + } + + @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("Expected Throwable to be thrown!"); + } catch (Throwable expected) { + // Should always be thrown the first time + } + + memoizer.compute(input); + } + + @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("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<>(computable); + RuntimeException runtimeException = new RuntimeException("Some runtime exception"); + expect(computable.compute(input)).andThrow(runtimeException); + replay(computable); + + 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); + + memoizer.compute(input); + } }