Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ protected AbstractConcurrentMemoizer(

@Override
public final R compute(final T input) {
return computeWith(input, () -> this.function.apply(input));
}

@Override
public final R computeWith(final T input, final Supplier<R> supplier) {
Objects.requireNonNull(supplier, "supplier must not be null");
final var key = input == null ? NULL_KEY : input;

// fast path — no lock needed on a cache hit
Expand All @@ -96,7 +102,7 @@ public final R compute(final T input) {
if (cached != null) {
return unwrap(cached);
}
final R result = this.function.apply(input);
final R result = supplier.get();
writeCache(key, wrap(result));
return result;
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

Expand All @@ -46,12 +47,18 @@ protected AbstractMemoizer(final Function<T, R> function, final Supplier<Map<Obj

@Override
public final R compute(final T input) {
return computeWith(input, () -> this.function.apply(input));
}

@Override
public final R computeWith(final T input, final Supplier<R> supplier) {
Objects.requireNonNull(supplier, "supplier must not be null");
final var key = input == null ? NULL_KEY : input;
final var cached = readCache(key);
if (cached != null) {
return unwrap(cached);
}
final R result = this.function.apply(input);
final R result = supplier.get();
writeCache(key, wrap(result));
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ public interface Memoizer<T, R> {
*/
R compute(T input);

/**
* Returns the memoized result for {@code input}, computing it via {@code supplier} on the
* first call for a given input. Unlike {@link #compute(Object)}, the computation is not fixed
* to the memoizer's underlying {@link Function} — this allows callers who don't have (or don't
* want to construct) a {@code Function<T, R>} up front to still share the same cache, keyed by
* {@code input}.
* <p>
* {@code supplier} is only invoked on a cache miss; on a hit, the cached result is returned and
* {@code supplier} is never called. If two callers race on the same {@code input} with different
* suppliers, only one supplier's result is memoized — the other is discarded.
*
* @param input the input; may be {@code null}
* @param supplier supplies the result on a cache miss; must not be {@code null}
* @return the memoized result; may be {@code null}
*/
R computeWith(T input, Supplier<R> supplier);

/**
* Clears all memoized results, forcing subsequent calls to recompute.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,67 @@ void shouldReportCorrectSize(final Function<Function<String, Integer>, Memoizer<
assertThat(memoizer.size()).isEqualTo(2);
}

// -------------------------------------------------------------------------
// computeWith
// -------------------------------------------------------------------------

@ParameterizedTest
@MethodSource("allFactories")
void shouldComputeWithSupplierOnFirstCall(final Function<Function<String, Integer>, Memoizer<String, Integer>> factory) {
final var memoizer = factory.apply(s -> { throw new AssertionError("function should not be invoked"); });
assertThat(memoizer.computeWith("hello", () -> 5)).isEqualTo(5);
}

@ParameterizedTest
@MethodSource("allFactories")
void shouldIgnoreSupplierOnCacheHit(final Function<Function<String, Integer>, Memoizer<String, Integer>> factory) {
final var memoizer = factory.apply(String::length);
memoizer.compute("hello");

assertThat(memoizer.computeWith("hello", () -> { throw new AssertionError("supplier should not be invoked"); }))
.isEqualTo(5);
}

@ParameterizedTest
@MethodSource("allFactories")
void shouldShareCacheBetweenComputeAndComputeWith(final Function<Function<String, Integer>, Memoizer<String, Integer>> factory) {
final var memoizer = factory.apply(String::length);
memoizer.computeWith("hello", () -> 5);

assertThat(memoizer.contains("hello")).isTrue();
assertThat(memoizer.compute("hello")).isEqualTo(5);
}

@ParameterizedTest
@MethodSource("allFactories")
void shouldRejectNullSupplier(final Function<Function<String, Integer>, Memoizer<String, Integer>> factory) {
final var memoizer = factory.apply(String::length);
assertThatThrownBy(() -> memoizer.computeWith("hello", null))
.isInstanceOf(NullPointerException.class);
}

@ParameterizedTest
@MethodSource("concurrentFactories")
void shouldComputeWithAtMostOnceUnderConcurrentLoad(
final Function<Function<String, Integer>, Memoizer<String, Integer>> factory) throws InterruptedException {
final var count = new AtomicInteger();
final var memoizer = factory.apply(s -> { throw new AssertionError("function should not be invoked"); });

final var threads = new Thread[20];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 50; j++) {
memoizer.computeWith("concurrent", () -> { count.incrementAndGet(); sleep10ms(); return 10; });
}
});
}
for (final var t : threads) t.start();
for (final var t : threads) t.join();

assertThat(count).hasValue(1);
assertThat(memoizer.size()).isEqualTo(1);
}

// -------------------------------------------------------------------------
// Construction
// -------------------------------------------------------------------------
Expand Down
Loading