Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To memoize a Java 8 function, wrap it in a function that stores results in a ConcurrentHashMap and uses computeIfAbsent to calculate each missing key. This works when the function is deterministic for the cache’s lifetime and the key captures every input that can affect its result.

Memoize a function with one argument

Java 8’s ConcurrentHashMap.computeIfAbsent provides the core operation: look up a key, compute a value if it is absent, and record a non-null result. Oracle’s Java SE 8 documentation says the entire invocation is atomic and the mapping function is applied at most once per key. It also cautions that computations should be short and simple and must not attempt to update other mappings in the same map. Oracle: ConcurrentHashMap (Java SE 8)

import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

public final class Memoizer {
    private Memoizer() {}

    public static <K, V> Function<K, V> memoize(
            Function<? super K, ? extends V> function) {
        ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
        return key -> cache.computeIfAbsent(key, function::apply);
    }
}

Call Memoizer.memoize(original) and use the returned function in place of the original. The first call for a key computes and stores its result; later calls for an equal key return the stored value. The cache belongs to that returned function, so creating a new memoized wrapper creates a new, empty cache.

What thread safety does—and does not—guarantee

ConcurrentHashMap.computeIfAbsent atomically handles the absent-key computation and insertion, and its Java 8 contract says the mapping function is applied at most once per key during that operation. This is useful when multiple threads call the wrapper concurrently. It does not make the wrapped function itself safe for unrelated concurrent use, nor does it provide expiry, eviction, or a maximum cache size. Oracle: ConcurrentHashMap (Java SE 8)

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep the mapping function short and avoid blocking work where contention matters: the API warns that other updates may be blocked while computation is in progress. Do not modify mappings in the same map from inside the mapping function. Detectably recursive updates can throw IllegalStateException.

Memoize a function with multiple arguments

Java’s standard Function accepts one argument. For a function of two arguments, combine both arguments in an immutable key, then adapt the original function to accept that key:

final class Pair<A, B> {
    final A first;
    final B second;

    Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }

    @Override public boolean equals(Object o) {
        if (!(o instanceof Pair)) return false;
        Pair<?, ?> p = (Pair<?, ?>) o;
        return java.util.Objects.equals(first, p.first)
            && java.util.Objects.equals(second, p.second);
    }

    @Override public int hashCode() {
        return java.util.Objects.hash(first, second);
    }
}
Function<Pair<A, B>, V> memoized =
    Memoizer.memoize(pair -> original.apply(pair.first, pair.second));

The key’s equals and hashCode must account for every input that changes the result. Do not use mutable key fields that can change after insertion: a changed hash or equality result can make a cached entry unreachable or cause incorrect reuse.

When memoization is correct

Memoization reuses a result; it does not know whether that result remains valid. Use it for stable, deterministic computations such as parsing, normalization, or pure recursive subproblems when the same inputs recur. Avoid it for functions whose results depend on time, I/O, randomness, locale, configuration, external state, mutable arguments, or side effects unless those dependencies are represented in the key and remain valid for the cache lifetime.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Confirm that equal keys always imply the same result for as long as the cache entry can be used.
  • Include every result-determining input in the key.
  • Keep keys immutable and stable after insertion.
  • Do not memoize a function merely because it is expensive; correctness comes first.

Null results and exceptions

ConcurrentHashMap does not accept null keys or values. If the mapping function returns null, computeIfAbsent records no mapping, so a later call for that key tries the computation again. The Java 8 ConcurrentMap documentation includes a memoization example using computeIfAbsent and specifies that a null result is not recorded. Oracle: ConcurrentMap (Java SE 8)

If null is a meaningful result, map it to a non-null sentinel or a non-null wrapper such as Optional<V>. If the mapping function throws, no value is established for that key; a later call can retry. Decide whether retrying is safe for the operation rather than assuming failures are cached.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Cache lifetime, size, and performance

The wrapper shown above is unbounded and has no time-to-live, refresh, persistence, or invalidation policy. If inputs can grow without limit, entries can accumulate for as long as the memoized function remains reachable. Add explicit removal or clearing when inputs or configuration change, or choose a cache design with size bounds or expiry when the workload requires them.

There is no universal memoization speedup: results depend on the cost of the function, how often keys repeat, key construction and lookup costs, memory use, and thread contention. Measure the actual workload on its target JVM and hardware before claiming a performance benefit.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Practical checklist

  • Verify that the function is deterministic for the intended cache lifetime.
  • Use an immutable key containing all inputs that affect the result.
  • Choose how to represent a meaningful null result.
  • Decide whether failed computations should be retried or represented explicitly.
  • Keep the mapping function from updating the same cache or performing unnecessary blocking work.
  • Set an invalidation, expiry, or size policy if the workload needs one.
  • Measure real hit patterns and performance rather than assuming caching will help.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.