diff --git a/src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java b/src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java new file mode 100644 index 000000000000..51479e5ae59c --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/caches/LIFOCache.java @@ -0,0 +1,562 @@ +package com.thealgorithms.datastructures.caches; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; + +/** + * A thread-safe generic cache implementation using the Last-In-First-Out eviction policy. + *

+ * The cache holds a fixed number of entries, defined by its capacity. When the cache is full and a + * new entry is added, the youngest entry in the cache is selected and evicted to make space. + *

+ * Optionally, entries can have a time-to-live (TTL) in milliseconds. If a TTL is set, entries will + * automatically expire and be removed upon access or insertion attempts. + *

+ * Features: + *

+ * + * @param the type of keys maintained by this cache + * @param the type of mapped values + * See LIFO + * @author Kevin Babu (GitHub) + */ +public final class LIFOCache { + + private final int capacity; + private final long defaultTTL; + private final Map> cache; + private final Lock lock; + private final Deque keys; + private long hits = 0; + private long misses = 0; + private final BiConsumer evictionListener; + private final EvictionStrategy evictionStrategy; + + /** + * Internal structure to store value + expiry timestamp. + * + * @param the type of the value being cached + */ + private static class CacheEntry { + V value; + long expiryTime; + + /** + * Constructs a new {@code CacheEntry} with the specified value and time-to-live (TTL). + * If TTL is 0, the entry is kept indefinitely, that is, unless it is the first value, + * then it will be removed according to the LIFO principle + * + * @param value the value to cache + * @param ttlMillis the time-to-live in milliseconds + */ + CacheEntry(V value, long ttlMillis) { + this.value = value; + if (ttlMillis == 0) { + this.expiryTime = Long.MAX_VALUE; + } else { + this.expiryTime = System.currentTimeMillis() + ttlMillis; + } + } + + /** + * Checks if the cache entry has expired. + * + * @return {@code true} if the current time is past the expiration time; {@code false} otherwise + */ + boolean isExpired() { + return System.currentTimeMillis() > expiryTime; + } + } + + /** + * Constructs a new {@code LIFOCache} instance using the provided {@link Builder}. + * + *

This constructor initializes the cache with the specified capacity and default TTL, + * sets up internal data structures (a {@code LinkedHashMap} for cache entries and configures eviction. + * + * @param builder the {@code Builder} object containing configuration parameters + */ + private LIFOCache(Builder builder) { + this.capacity = builder.capacity; + this.defaultTTL = builder.defaultTTL; + this.cache = HashMap.newHashMap(builder.capacity); + this.keys = new ArrayDeque<>(builder.capacity); + this.lock = new ReentrantLock(); + this.evictionListener = builder.evictionListener; + this.evictionStrategy = builder.evictionStrategy; + } + + /** + * Retrieves the value associated with the specified key from the cache. + * + *

If the key is not present or the corresponding entry has expired, this method + * returns {@code null}. If an expired entry is found, it will be removed and the + * eviction listener (if any) will be notified. Cache hit-and-miss statistics are + * also updated accordingly. + * + * @param key the key whose associated value is to be returned; must not be {@code null} + * @return the cached value associated with the key, or {@code null} if not present or expired + * @throws IllegalArgumentException if {@code key} is {@code null} + */ + public V get(K key) { + if (key == null) { + throw new IllegalArgumentException("Key must not be null"); + } + + lock.lock(); + try { + evictionStrategy.onAccess(this); + + CacheEntry entry = cache.get(key); + if (entry == null || entry.isExpired()) { + if (entry != null) { + cache.remove(key); + keys.remove(key); + notifyEviction(key, entry.value); + } + misses++; + return null; + } + hits++; + return entry.value; + } finally { + lock.unlock(); + } + } + + /** + * Adds a key-value pair to the cache using the default time-to-live (TTL). + * + *

The key may overwrite an existing entry. The actual insertion is delegated + * to the overloaded {@link #put(K, V, long)} method. + * + * @param key the key to cache the value under + * @param value the value to be cached + */ + public void put(K key, V value) { + put(key, value, defaultTTL); + } + + /** + * Adds a key-value pair to the cache with a specified time-to-live (TTL). + * + *

If the key already exists, its value is removed, re-inserted at tail and its TTL is reset. + * If the key does not exist and the cache is full, the youngest entry is evicted to make space. + * Expired entries are also cleaned up prior to any eviction. The eviction listener + * is notified when an entry gets evicted. + * + * @param key the key to associate with the cached value; must not be {@code null} + * @param value the value to be cached; must not be {@code null} + * @param ttlMillis the time-to-live for this entry in milliseconds; must be >= 0 + * @throws IllegalArgumentException if {@code key} or {@code value} is {@code null}, or if {@code ttlMillis} is negative + */ + public void put(K key, V value, long ttlMillis) { + if (key == null || value == null) { + throw new IllegalArgumentException("Key and value must not be null"); + } + if (ttlMillis < 0) { + throw new IllegalArgumentException("TTL must be >= 0"); + } + + lock.lock(); + try { + // If key already exists, remove it. It will later be re-inserted at top of stack + keys.remove(key); + CacheEntry oldEntry = cache.remove(key); + if (oldEntry != null && !oldEntry.isExpired()) { + notifyEviction(key, oldEntry.value); + } + + // Evict expired entries to make space for new entry + evictExpired(); + + // If no expired entry was removed, remove the youngest + if (cache.size() >= capacity) { + K youngestKey = keys.pollLast(); + CacheEntry youngestEntry = cache.remove(youngestKey); + notifyEviction(youngestKey, youngestEntry.value); + } + + // Insert new entry at tail + keys.add(key); + cache.put(key, new CacheEntry<>(value, ttlMillis)); + } finally { + lock.unlock(); + } + } + + /** + * Removes all expired entries from the cache. + * + *

This method iterates through the list of cached keys and checks each associated + * entry for expiration. Expired entries are removed the cache map. For each eviction, + * the eviction listener is notified. + */ + private int evictExpired() { + int count = 0; + Iterator it = keys.iterator(); + + while (it.hasNext()) { + K k = it.next(); + CacheEntry entry = cache.get(k); + if (entry != null && entry.isExpired()) { + it.remove(); + cache.remove(k); + notifyEviction(k, entry.value); + count++; + } + } + + return count; + } + + /** + * Removes the specified key and its associated entry from the cache. + * + * @param key the key to remove from the cache; + * @return the value associated with the key; or {@code null} if no such key exists + */ + public V removeKey(K key) { + if (key == null) { + throw new IllegalArgumentException("Key cannot be null"); + } + lock.lock(); + try { + CacheEntry entry = cache.remove(key); + keys.remove(key); + + // No such key in cache + if (entry == null) { + return null; + } + + notifyEviction(key, entry.value); + return entry.value; + } finally { + lock.unlock(); + } + } + + /** + * Notifies the eviction listener, if one is registered, that a key-value pair has been evicted. + * + *

If the {@code evictionListener} is not {@code null}, it is invoked with the provided key + * and value. Any exceptions thrown by the listener are caught and logged to standard error, + * preventing them from disrupting cache operations. + * + * @param key the key that was evicted + * @param value the value that was associated with the evicted key + */ + private void notifyEviction(K key, V value) { + if (evictionListener != null) { + try { + evictionListener.accept(key, value); + } catch (Exception e) { + System.err.println("Eviction listener failed: " + e.getMessage()); + } + } + } + + /** + * Returns the number of successful cache lookups (hits). + * + * @return the number of cache hits + */ + public long getHits() { + lock.lock(); + try { + return hits; + } finally { + lock.unlock(); + } + } + + /** + * Returns the number of failed cache lookups (misses), including expired entries. + * + * @return the number of cache misses + */ + public long getMisses() { + lock.lock(); + try { + return misses; + } finally { + lock.unlock(); + } + } + + /** + * Returns the current number of entries in the cache, excluding expired ones. + * + * @return the current cache size + */ + public int size() { + lock.lock(); + try { + evictionStrategy.onAccess(this); + + int count = 0; + for (CacheEntry entry : cache.values()) { + if (!entry.isExpired()) { + ++count; + } + } + return count; + } finally { + lock.unlock(); + } + } + + /** + * Removes all entries from the cache, regardless of their expiration status. + * + *

This method clears the internal cache map entirely, resets the hit-and-miss counters, + * and notifies the eviction listener (if any) for each removed entry. + * Note that expired entries are treated the same as active ones for the purpose of clearing. + * + *

This operation acquires the internal lock to ensure thread safety. + */ + public void clear() { + lock.lock(); + try { + for (Map.Entry> entry : cache.entrySet()) { + notifyEviction(entry.getKey(), entry.getValue().value); + } + keys.clear(); + cache.clear(); + hits = 0; + misses = 0; + } finally { + lock.unlock(); + } + } + + /** + * Returns a set of all keys currently stored in the cache that have not expired. + * + *

This method iterates through the cache and collects the keys of all non-expired entries. + * Expired entries are ignored but not removed. If you want to ensure expired entries are cleaned up, + * consider invoking {@link EvictionStrategy#onAccess(LIFOCache)} or calling {@link #evictExpired()} manually. + * + *

This operation acquires the internal lock to ensure thread safety. + * + * @return a set containing all non-expired keys currently in the cache + */ + public Set getAllKeys() { + lock.lock(); + try { + Set result = new HashSet<>(); + + for (Map.Entry> entry : cache.entrySet()) { + if (!entry.getValue().isExpired()) { + result.add(entry.getKey()); + } + } + + return result; + } finally { + lock.unlock(); + } + } + + /** + * Returns the current {@link EvictionStrategy} used by this cache instance. + + * @return the eviction strategy currently assigned to this cache + */ + public EvictionStrategy getEvictionStrategy() { + return evictionStrategy; + } + + /** + * Returns a string representation of the cache, including metadata and current non-expired entries. + * + *

The returned string includes the cache's capacity, current size (excluding expired entries), + * hit-and-miss counts, and a map of all non-expired key-value pairs. This method acquires a lock + * to ensure thread-safe access. + * + * @return a string summarizing the state of the cache + */ + @Override + public String toString() { + lock.lock(); + try { + Map visible = new LinkedHashMap<>(); + for (Map.Entry> entry : cache.entrySet()) { + if (!entry.getValue().isExpired()) { + visible.put(entry.getKey(), entry.getValue().value); + } + } + return String.format("Cache(capacity=%d, size=%d, hits=%d, misses=%d, entries=%s)", capacity, visible.size(), hits, misses, visible); + } finally { + lock.unlock(); + } + } + + /** + * A strategy interface for controlling when expired entries are evicted from the cache. + * + *

Implementations decide whether and when to trigger {@link LIFOCache#evictExpired()} based + * on cache usage patterns. This allows for flexible eviction behaviour such as periodic cleanup, + * or no automatic cleanup. + * + * @param the type of keys maintained by the cache + * @param the type of cached values + */ + public interface EvictionStrategy { + /** + * Called on each cache access (e.g., {@link LIFOCache#get(Object)}) to optionally trigger eviction. + * + * @param cache the cache instance on which this strategy is applied + * @return the number of expired entries evicted during this access + */ + int onAccess(LIFOCache cache); + } + + /** + * An eviction strategy that performs eviction of expired entries on each call. + * + * @param the type of keys + * @param the type of values + */ + public static class ImmediateEvictionStrategy implements EvictionStrategy { + @Override + public int onAccess(LIFOCache cache) { + return cache.evictExpired(); + } + } + + /** + * An eviction strategy that triggers eviction on every fixed number of accesses. + * + *

This deterministic strategy ensures cleanup occurs at predictable intervals, + * ideal for moderately active caches where memory usage is a concern. + * + * @param the type of keys + * @param the type of values + */ + public static class PeriodicEvictionStrategy implements EvictionStrategy { + private final int interval; + private final AtomicInteger counter = new AtomicInteger(); + + /** + * Constructs a periodic eviction strategy. + * + * @param interval the number of accesses between evictions; must be > 0 + * @throws IllegalArgumentException if {@code interval} is less than or equal to 0 + */ + public PeriodicEvictionStrategy(int interval) { + if (interval <= 0) { + throw new IllegalArgumentException("Interval must be > 0"); + } + this.interval = interval; + } + + @Override + public int onAccess(LIFOCache cache) { + if (counter.incrementAndGet() % interval == 0) { + return cache.evictExpired(); + } + + return 0; + } + } + + /** + * A builder for constructing a {@link LIFOCache} instance with customizable settings. + * + *

Allows configuring capacity, default TTL, eviction listener, and a pluggable eviction + * strategy. Call {@link #build()} to create the configured cache instance. + * + * @param the type of keys maintained by the cache + * @param the type of values stored in the cache + */ + public static class Builder { + private final int capacity; + private long defaultTTL = 0; + private BiConsumer evictionListener; + private EvictionStrategy evictionStrategy = new LIFOCache.ImmediateEvictionStrategy<>(); + /** + * Creates a new {@code Builder} with the specified cache capacity. + * + * @param capacity the maximum number of entries the cache can hold; must be > 0 + * @throws IllegalArgumentException if {@code capacity} is less than or equal to 0 + */ + public Builder(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be > 0"); + } + this.capacity = capacity; + } + + /** + * Sets the default time-to-live (TTL) in milliseconds for cache entries. + * + * @param ttlMillis the TTL duration in milliseconds; must be >= 0 + * @return this builder instance for chaining + * @throws IllegalArgumentException if {@code ttlMillis} is negative + */ + public Builder defaultTTL(long ttlMillis) { + if (ttlMillis < 0) { + throw new IllegalArgumentException("Default TTL must be >= 0"); + } + this.defaultTTL = ttlMillis; + return this; + } + + /** + * Sets an eviction listener to be notified when entries are evicted from the cache. + * + * @param listener a {@link BiConsumer} that accepts evicted keys and values; must not be {@code null} + * @return this builder instance for chaining + * @throws IllegalArgumentException if {@code listener} is {@code null} + */ + public Builder evictionListener(BiConsumer listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener must not be null"); + } + this.evictionListener = listener; + return this; + } + + /** + * Builds and returns a new {@link LIFOCache} instance with the configured parameters. + * + * @return a fully configured {@code LIFOCache} instance + */ + public LIFOCache build() { + return new LIFOCache<>(this); + } + + /** + * Sets the eviction strategy used to determine when to clean up expired entries. + * + * @param strategy an {@link EvictionStrategy} implementation; must not be {@code null} + * @return this builder instance + * @throws IllegalArgumentException if {@code strategy} is {@code null} + */ + public Builder evictionStrategy(EvictionStrategy strategy) { + if (strategy == null) { + throw new IllegalArgumentException("Eviction strategy must not be null"); + } + this.evictionStrategy = strategy; + return this; + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java b/src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java new file mode 100644 index 000000000000..df60a393b136 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/caches/LIFOCacheTest.java @@ -0,0 +1,341 @@ +package com.thealgorithms.datastructures.caches; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +class LIFOCacheTest { + private LIFOCache cache; + private Set evictedKeys; + private List evictedValues; + + @BeforeEach + void setUp() { + evictedKeys = new HashSet<>(); + evictedValues = new ArrayList<>(); + + cache = new LIFOCache.Builder(3) + .defaultTTL(1000) + .evictionListener((k, v) -> { + evictedKeys.add(k); + evictedValues.add(v); + }) + .build(); + } + + @Test + void testPutAndGet() { + cache.put("a", "apple"); + Assertions.assertEquals("apple", cache.get("a")); + } + + @Test + void testOverwriteValue() { + cache.put("a", "apple"); + cache.put("a", "avocado"); + Assertions.assertEquals("avocado", cache.get("a")); + } + + @Test + void testExpiration() throws InterruptedException { + cache.put("temp", "value", 100); + Thread.sleep(200); + Assertions.assertNull(cache.get("temp")); + Assertions.assertTrue(evictedKeys.contains("temp")); + } + + @Test + void testEvictionOnCapacity() { + cache.put("a", "alpha"); + cache.put("b", "bravo"); + cache.put("c", "charlie"); + cache.put("d", "delta"); + + int size = cache.size(); + Assertions.assertEquals(3, size); + Assertions.assertEquals(1, evictedKeys.size()); + Assertions.assertEquals(1, evictedValues.size()); + Assertions.assertEquals("charlie", evictedValues.getFirst()); + } + + @Test + void testEvictionListener() { + cache.put("x", "one"); + cache.put("y", "two"); + cache.put("z", "three"); + cache.put("w", "four"); + + Assertions.assertFalse(evictedKeys.isEmpty()); + Assertions.assertFalse(evictedValues.isEmpty()); + } + + @Test + void testHitsAndMisses() { + cache.put("a", "apple"); + Assertions.assertEquals("apple", cache.get("a")); + Assertions.assertNull(cache.get("b")); + Assertions.assertEquals(1, cache.getHits()); + Assertions.assertEquals(1, cache.getMisses()); + } + + @Test + void testSizeExcludesExpired() throws InterruptedException { + cache.put("a", "a", 100); + cache.put("b", "b", 100); + cache.put("c", "c", 100); + Thread.sleep(150); + Assertions.assertEquals(0, cache.size()); + } + + @Test + void testSizeIncludesFresh() { + cache.put("a", "a", 1000); + cache.put("b", "b", 1000); + cache.put("c", "c", 1000); + Assertions.assertEquals(3, cache.size()); + } + + @Test + void testToStringDoesNotExposeExpired() throws InterruptedException { + cache.put("live", "alive"); + cache.put("dead", "gone", 100); + Thread.sleep(150); + String result = cache.toString(); + Assertions.assertTrue(result.contains("live")); + Assertions.assertFalse(result.contains("dead")); + } + + @Test + void testNullKeyGetThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.get(null)); + } + + @Test + void testPutNullKeyThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.put(null, "v")); + } + + @Test + void testPutNullValueThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.put("k", null)); + } + + @Test + void testPutNegativeTTLThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.put("k", "v", -1)); + } + + @Test + void testBuilderNegativeCapacityThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new LIFOCache.Builder<>(0)); + } + + @Test + void testBuilderNullEvictionListenerThrows() { + LIFOCache.Builder builder = new LIFOCache.Builder<>(1); + Assertions.assertThrows(IllegalArgumentException.class, () -> builder.evictionListener(null)); + } + + @Test + void testEvictionListenerExceptionDoesNotCrash() { + LIFOCache listenerCache = new LIFOCache.Builder(1).evictionListener((k, v) -> { throw new RuntimeException("Exception"); }).build(); + + listenerCache.put("a", "a"); + listenerCache.put("b", "b"); + Assertions.assertDoesNotThrow(() -> listenerCache.get("a")); + } + + @Test + void testTtlZeroThrowsIllegalArgumentException() { + Executable exec = () -> new LIFOCache.Builder(3).defaultTTL(-1).build(); + Assertions.assertThrows(IllegalArgumentException.class, exec); + } + + @Test + void testPeriodicEvictionStrategyEvictsAtInterval() throws InterruptedException { + LIFOCache periodicCache = new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(new LIFOCache.PeriodicEvictionStrategy<>(3)).build(); + + periodicCache.put("x", "1"); + Thread.sleep(100); + int ev1 = periodicCache.getEvictionStrategy().onAccess(periodicCache); + int ev2 = periodicCache.getEvictionStrategy().onAccess(periodicCache); + int ev3 = periodicCache.getEvictionStrategy().onAccess(periodicCache); + + Assertions.assertEquals(0, ev1); + Assertions.assertEquals(0, ev2); + Assertions.assertEquals(1, ev3, "Eviction should happen on the 3rd access"); + Assertions.assertEquals(0, periodicCache.size()); + } + + @Test + void testPeriodicEvictionStrategyThrowsExceptionIfIntervalLessThanOrEqual0() { + Executable executable = () -> new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(new LIFOCache.PeriodicEvictionStrategy<>(0)).build(); + + Assertions.assertThrows(IllegalArgumentException.class, executable); + } + + @Test + void testImmediateEvictionStrategyStrategyEvictsOnEachCall() throws InterruptedException { + LIFOCache immediateEvictionStrategy = new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(new LIFOCache.ImmediateEvictionStrategy<>()).build(); + + immediateEvictionStrategy.put("x", "1"); + Thread.sleep(100); + int evicted = immediateEvictionStrategy.getEvictionStrategy().onAccess(immediateEvictionStrategy); + + Assertions.assertEquals(1, evicted); + } + + @Test + void testBuilderThrowsExceptionIfEvictionStrategyNull() { + Executable executable = () -> new LIFOCache.Builder(10).defaultTTL(50).evictionStrategy(null).build(); + + Assertions.assertThrows(IllegalArgumentException.class, executable); + } + + @Test + void testReturnsCorrectStrategyInstance() { + LIFOCache.EvictionStrategy strategy = new LIFOCache.ImmediateEvictionStrategy<>(); + + LIFOCache newCache = new LIFOCache.Builder(10).defaultTTL(1000).evictionStrategy(strategy).build(); + + Assertions.assertSame(strategy, newCache.getEvictionStrategy(), "Returned strategy should be the same instance"); + } + + @Test + void testDefaultStrategyIsImmediateEvictionStrategy() { + LIFOCache newCache = new LIFOCache.Builder(5).defaultTTL(1000).build(); + + Assertions.assertInstanceOf(LIFOCache.ImmediateEvictionStrategy.class, newCache.getEvictionStrategy(), "Default strategy should be ImmediateEvictionStrategyStrategy"); + } + + @Test + void testGetEvictionStrategyIsNotNull() { + LIFOCache newCache = new LIFOCache.Builder(5).build(); + + Assertions.assertNotNull(newCache.getEvictionStrategy(), "Eviction strategy should never be null"); + } + + @Test + void testRemoveKeyRemovesExistingKey() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + + Assertions.assertEquals("Alpha", cache.get("A")); + Assertions.assertEquals("Beta", cache.get("B")); + + String removed = cache.removeKey("A"); + Assertions.assertEquals("Alpha", removed); + + Assertions.assertNull(cache.get("A")); + Assertions.assertEquals(1, cache.size()); + } + + @Test + void testRemoveKeyReturnsNullIfKeyNotPresent() { + cache.put("X", "X-ray"); + + Assertions.assertNull(cache.removeKey("NonExistent")); + Assertions.assertEquals(1, cache.size()); + } + + @Test + void testRemoveKeyHandlesExpiredEntry() throws InterruptedException { + LIFOCache expiringCache = new LIFOCache.Builder(2).defaultTTL(100).evictionStrategy(new LIFOCache.ImmediateEvictionStrategy<>()).build(); + + expiringCache.put("T", "Temporary"); + + Thread.sleep(200); + + String removed = expiringCache.removeKey("T"); + Assertions.assertEquals("Temporary", removed); + Assertions.assertNull(expiringCache.get("T")); + } + + @Test + void testRemoveKeyThrowsIfKeyIsNull() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cache.removeKey(null)); + } + + @Test + void testRemoveKeyTriggersEvictionListener() { + AtomicInteger evictedCount = new AtomicInteger(); + + LIFOCache localCache = new LIFOCache.Builder(2).evictionListener((key, value) -> evictedCount.incrementAndGet()).build(); + + localCache.put("A", "Apple"); + localCache.put("B", "Banana"); + + localCache.removeKey("A"); + + Assertions.assertEquals(1, evictedCount.get(), "Eviction listener should have been called once"); + } + + @Test + void testRemoveKeyDoestNotAffectOtherKeys() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("C", "Gamma"); + + cache.removeKey("B"); + + Assertions.assertEquals("Alpha", cache.get("A")); + Assertions.assertNull(cache.get("B")); + Assertions.assertEquals("Gamma", cache.get("C")); + } + + @Test + void testEvictionListenerExceptionDoesNotPropagate() { + LIFOCache localCache = new LIFOCache.Builder(1).evictionListener((key, value) -> { throw new RuntimeException(); }).build(); + + localCache.put("A", "Apple"); + + Assertions.assertDoesNotThrow(() -> localCache.put("B", "Beta")); + } + + @Test + void testGetKeysReturnsAllFreshKeys() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("G", "Gamma"); + + Set expectedKeys = Set.of("A", "B", "G"); + Assertions.assertEquals(expectedKeys, cache.getAllKeys()); + } + + @Test + void testGetKeysIgnoresExpiredKeys() throws InterruptedException { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("G", "Gamma", 100); + + Set expectedKeys = Set.of("A", "B"); + Thread.sleep(200); + Assertions.assertEquals(expectedKeys, cache.getAllKeys()); + } + + @Test + void testClearRemovesAllEntries() { + cache.put("A", "Alpha"); + cache.put("B", "Beta"); + cache.put("G", "Gamma"); + + cache.clear(); + Assertions.assertEquals(0, cache.size()); + } + + @Test + void testGetExpiredKeyIncrementsMissesCount() throws InterruptedException { + LIFOCache localCache = new LIFOCache.Builder(3).evictionStrategy(cache -> 0).defaultTTL(10).build(); + localCache.put("A", "Alpha"); + Thread.sleep(100); + String value = localCache.get("A"); + Assertions.assertEquals(1, localCache.getMisses()); + Assertions.assertNull(value); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy