3030 *
3131 * @param <K> the type of keys maintained by this cache
3232 * @param <V> the type of mapped values
33- *
3433 * See <a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Random_replacement_(RR)">Random Replacement</a>
3534 * @author Kevin Babu (<a href="https://www.github.com/KevinMwita7">GitHub</a>)
3635 */
@@ -45,8 +44,8 @@ public final class RRCache<K, V> {
4544
4645 private long hits = 0 ;
4746 private long misses = 0 ;
48-
4947 private final BiConsumer <K , V > evictionListener ;
48+ private final EvictionStrategy <K , V > evictionStrategy ;
5049
5150 /**
5251 * Internal structure to store value + expiry timestamp.
@@ -95,6 +94,7 @@ private RRCache(Builder<K, V> builder) {
9594 this .random = builder .random != null ? builder .random : new Random ();
9695 this .lock = new ReentrantLock ();
9796 this .evictionListener = builder .evictionListener ;
97+ this .evictionStrategy = builder .evictionStrategy ;
9898 }
9999
100100 /**
@@ -116,6 +116,8 @@ public V get(K key) {
116116
117117 lock .lock ();
118118 try {
119+ evictionStrategy .onAccess (this );
120+
119121 CacheEntry <V > entry = cache .get (key );
120122 if (entry == null || entry .isExpired ()) {
121123 if (entry != null ) {
@@ -196,17 +198,22 @@ public void put(K key, V value, long ttlMillis) {
196198 * entry for expiration. Expired entries are removed from both the key tracking list
197199 * and the cache map. For each eviction, the eviction listener is notified.
198200 */
199- private void evictExpired () {
201+ private int evictExpired () {
200202 Iterator <K > it = keys .iterator ();
203+ int expiredCount = 0 ;
204+
201205 while (it .hasNext ()) {
202206 K k = it .next ();
203207 CacheEntry <V > entry = cache .get (k );
204208 if (entry != null && entry .isExpired ()) {
205209 it .remove ();
206210 cache .remove (k );
211+ ++expiredCount ;
207212 notifyEviction (k , entry .value );
208213 }
209214 }
215+
216+ return expiredCount ;
210217 }
211218
212219 /**
@@ -277,6 +284,13 @@ public long getMisses() {
277284 public int size () {
278285 lock .lock ();
279286 try {
287+ int cachedSize = cache .size ();
288+ int evictedCount = evictionStrategy .onAccess (this );
289+ if (evictedCount > 0 ) {
290+ return cachedSize - evictedCount ;
291+ }
292+
293+ // This runs if periodic eviction does not occur
280294 int count = 0 ;
281295 for (Map .Entry <K , CacheEntry <V >> entry : cache .entrySet ()) {
282296 if (!entry .getValue ().isExpired ()) {
@@ -315,11 +329,78 @@ public String toString() {
315329 }
316330
317331 /**
318- * A builder for creating instances of {@link RRCache} with custom configuration .
332+ * A strategy interface for controlling when expired entries are evicted from the cache .
319333 *
320- * <p>This static inner class allows you to configure parameters such as cache capacity,
321- * default TTL (time-to-live), random eviction behavior, and an optional eviction listener.
322- * Once configured, use {@link #build()} to create the {@code RRCache} instance.
334+ * <p>Implementations decide whether and when to trigger {@link RRCache#evictExpired()} based
335+ * on cache usage patterns. This allows for flexible eviction behaviour such as periodic cleanup,
336+ * or no automatic cleanup.
337+ *
338+ * @param <K> the type of keys maintained by the cache
339+ * @param <V> the type of cached values
340+ */
341+ public interface EvictionStrategy <K , V > {
342+ /**
343+ * Called on each cache access (e.g., {@link RRCache#get(Object)}) to optionally trigger eviction.
344+ *
345+ * @param cache the cache instance on which this strategy is applied
346+ * @return the number of expired entries evicted during this access
347+ */
348+ int onAccess (RRCache <K , V > cache );
349+ }
350+
351+ /**
352+ * An eviction strategy that performs eviction of expired entries on each call.
353+ *
354+ * @param <K> the type of keys
355+ * @param <V> the type of values
356+ */
357+ public static class NoEvictionStrategy <K , V > implements EvictionStrategy <K , V > {
358+ @ Override public int onAccess (RRCache <K , V > cache ) {
359+ return cache .evictExpired ();
360+ }
361+ }
362+
363+ /**
364+ * An eviction strategy that triggers eviction every fixed number of accesses.
365+ *
366+ * <p>This deterministic strategy ensures cleanup occurs at predictable intervals,
367+ * ideal for moderately active caches where memory usage is a concern.
368+ *
369+ * @param <K> the type of keys
370+ * @param <V> the type of values
371+ */
372+ public static class PeriodicEvictionStrategy <K , V > implements EvictionStrategy <K , V > {
373+ private final int interval ;
374+ private int counter = 0 ;
375+
376+ /**
377+ * Constructs a periodic eviction strategy.
378+ *
379+ * @param interval the number of accesses between evictions; must be > 0
380+ * @throws IllegalArgumentException if {@code interval} is less than or equal to 0
381+ */
382+ public PeriodicEvictionStrategy (int interval ) {
383+ if (interval <= 0 ) {
384+ throw new IllegalArgumentException ("Interval must be > 0" );
385+ }
386+ this .interval = interval ;
387+ }
388+
389+ @ Override
390+ public int onAccess (RRCache <K , V > cache ) {
391+ if (++counter % interval == 0 ) {
392+ return cache .evictExpired ();
393+ }
394+
395+ return 0 ;
396+ }
397+ }
398+
399+ /**
400+ * A builder for constructing an {@link RRCache} instance with customizable settings.
401+ *
402+ * <p>Allows configuring capacity, default TTL, random eviction behavior, eviction listener,
403+ * and a pluggable eviction strategy. Call {@link #build()} to create the configured cache instance.
323404 *
324405 * @param <K> the type of keys maintained by the cache
325406 * @param <V> the type of values stored in the cache
@@ -329,7 +410,7 @@ public static class Builder<K, V> {
329410 private long defaultTTL = 0 ;
330411 private Random random ;
331412 private BiConsumer <K , V > evictionListener ;
332-
413+ private EvictionStrategy < K , V > evictionStrategy = new RRCache . PeriodicEvictionStrategy <>( 100 );
333414 /**
334415 * Creates a new {@code Builder} with the specified cache capacity.
335416 *
@@ -396,5 +477,20 @@ public Builder<K, V> evictionListener(BiConsumer<K, V> listener) {
396477 public RRCache <K , V > build () {
397478 return new RRCache <>(this );
398479 }
480+
481+ /**
482+ * Sets the eviction strategy used to determine when to clean up expired entries.
483+ *
484+ * @param strategy an {@link EvictionStrategy} implementation; must not be {@code null}
485+ * @return this builder instance
486+ * @throws IllegalArgumentException if {@code strategy} is {@code null}
487+ */
488+ public Builder <K , V > evictionStrategy (EvictionStrategy <K , V > strategy ) {
489+ if (strategy == null ) {
490+ throw new IllegalArgumentException ("Eviction strategy must not be null" );
491+ }
492+ this .evictionStrategy = strategy ;
493+ return this ;
494+ }
399495 }
400496}
0 commit comments