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 @@ -20,6 +20,7 @@
* #L%
*/

import build.base.foundation.memoizer.Memoizer;
import build.base.foundation.stream.Streamable;

import java.lang.annotation.Annotation;
Expand Down Expand Up @@ -143,13 +144,17 @@ public final class Introspection {
defaultValues.put(String.class, "");
DEFAULT_VALUES = java.util.Collections.unmodifiableMap(defaultValues);

ALL_DECLARED_FIELDS_BY_CLASS = new Memoizer<>(clazz -> extractAll(clazz, Class::getDeclaredFields));
ALL_DECLARED_FIELDS_BY_CLASS = Memoizer.<Class<?>, Streamable<Field>>of(
clazz -> extractAll(clazz, Class::getDeclaredFields)).concurrent().build();

ALL_DECLARED_METHODS_BY_CLASS = new Memoizer<>(clazz -> extractAll(clazz, Class::getDeclaredMethods));
ALL_DECLARED_METHODS_BY_CLASS = Memoizer.<Class<?>, Streamable<Method>>of(
clazz -> extractAll(clazz, Class::getDeclaredMethods)).concurrent().build();

ALL_DECLARED_ANNOTATIONS_BY_CLASS = new Memoizer<>(clazz ->
new Memoizer<>(annotationClass ->
extractAll(clazz, c -> c.getDeclaredAnnotationsByType(annotationClass))));
ALL_DECLARED_ANNOTATIONS_BY_CLASS = Memoizer.<Class<?>, Memoizer<Class<? extends Annotation>, Streamable<? extends Annotation>>>of(
clazz -> Memoizer.<Class<? extends Annotation>, Streamable<? extends Annotation>>of(
annotationClass -> extractAll(clazz, c -> c.getDeclaredAnnotationsByType(annotationClass))
).concurrent().build()
).concurrent().build();
}

/**
Expand Down
158 changes: 0 additions & 158 deletions base-foundation/src/main/java/build/base/foundation/Memoizer.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package build.base.foundation.memoizer;

/*-
* #%L
* base.build Foundation
* %%
* Copyright (C) 2026 Workday Inc
* %%
* Licensed 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.
* #L%
*/

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;

/**
* Abstract base for thread-safe {@link Memoizer} implementations.
* <p>
* Uses a per-key {@link ReentrantLock} to synchronize cache misses. Locks are allocated on first
* miss and removed after the result is stored, keeping the lock map lean. Cache hits follow a
* lock-free fast path.
* <p>
* {@link ReentrantLock} is used rather than {@code ConcurrentHashMap.computeIfAbsent} because the
* memoized function may re-enter this memoizer for a key that hashes to the same map bin.
* {@code computeIfAbsent} holds the bin lock during computation, so a re-entrant call on the same
* bin would deadlock or throw {@link IllegalStateException}. The per-key lock approach holds no bin
* lock during computation, so re-entrant calls always succeed.
* <p>
* The supplied cache {@link Map} must implement {@link ConcurrentMap} to ensure the fast-path read
* is visible across threads without holding a lock.
* <p>
* Subclasses may customize cache access by overriding {@link #readCache(Object)} and
* {@link #writeCache(Object, Object)} — for example, to wrap results in
* {@link java.lang.ref.SoftReference}s as {@link SoftConcurrentMemoizer} does.
*
* @param <T> the input type
* @param <R> the result type
* @author reed.vonredwitz
* @since Jun-2026
*/
public abstract class AbstractConcurrentMemoizer<T, R> extends BaseMemoizer<T, R> {

private final ConcurrentMap<Object, ReentrantLock> computeLocks;

/**
* Constructs an {@link AbstractConcurrentMemoizer}.
*
* @param function the function to memoize; must not be {@code null}
* @param mapSupplier supplies the backing cache {@link ConcurrentMap}; must not be {@code null}
* @param locksSupplier supplies the per-key lock {@link ConcurrentMap}; must not be {@code null}
* @throws IllegalArgumentException if the map returned by {@code mapSupplier} does not implement
* {@link ConcurrentMap}
*/
protected AbstractConcurrentMemoizer(
final Function<T, R> function,
final Supplier<Map<Object, Object>> mapSupplier,
final Supplier<ConcurrentMap<Object, ReentrantLock>> locksSupplier) {
super(function, mapSupplier);
if (!(this.cache instanceof ConcurrentMap)) {
throw new IllegalArgumentException(
"The supplied Map must implement ConcurrentMap to support lock-free reads");
}
this.computeLocks = Objects.requireNonNull(
Objects.requireNonNull(locksSupplier, "locksSupplier must not be null").get(),
"the supplied ConcurrentMap must not be null");
}

@Override
public final R compute(final T input) {
final var key = input == null ? NULL_KEY : input;

// fast path — no lock needed on a cache hit
var cached = readCache(key);
if (cached != null) {
return unwrap(cached);
}

final var lock = this.computeLocks.computeIfAbsent(key, k -> new ReentrantLock());
lock.lock();
try {
cached = readCache(key);
if (cached != null) {
return unwrap(cached);
}
final R result = this.function.apply(input);
writeCache(key, wrap(result));
return result;
} finally {
lock.unlock();
this.computeLocks.remove(key);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package build.base.foundation.memoizer;

/*-
* #%L
* base.build Foundation
* %%
* Copyright (C) 2026 Workday Inc
* %%
* Licensed 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.
* #L%
*/

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

/**
* Abstract base for non-thread-safe {@link Memoizer} implementations.
* <p>
* Subclasses may customize cache access by overriding {@link #readCache(Object)} and
* {@link #writeCache(Object, Object)} — for example, to wrap results in
* {@link java.lang.ref.SoftReference}s as {@link SoftMemoizer} does.
* <p>
* This class is <em>not</em> thread-safe. For concurrent use see {@link AbstractConcurrentMemoizer}.
*
* @param <T> the input type
* @param <R> the result type
* @author reed.vonredwitz
* @since Jun-2026
*/
public abstract class AbstractMemoizer<T, R> extends BaseMemoizer<T, R> {

protected AbstractMemoizer(final Function<T, R> function, final Supplier<Map<Object, Object>> mapSupplier) {
super(function, mapSupplier);
}

@Override
public final R compute(final T input) {
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);
writeCache(key, wrap(result));
return result;
}
}
Loading
Loading