Skip to content

refactor: Enhance docs, add more tests in LRUCache #5950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 23, 2024
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 @@ -4,15 +4,40 @@
import java.util.Map;

/**
* Least recently used (LRU)
* <p>
* Discards the least recently used items first. This algorithm requires keeping
* track of what was used when, which is expensive if one wants to make sure the
* algorithm always discards the least recently used item.
* https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
* A Least Recently Used (LRU) Cache implementation.
*
* @param <K> key type
* @param <V> value type
* <p>An LRU cache is a fixed-size cache that maintains items in order of use. When the cache reaches
* its capacity and a new item needs to be added, it removes the least recently used item first.
* This implementation provides O(1) time complexity for both get and put operations.</p>
*
* <p>Features:</p>
* <ul>
* <li>Fixed-size cache with configurable capacity</li>
* <li>Constant time O(1) operations for get and put</li>
* <li>Thread-unsafe - should be externally synchronized if used in concurrent environments</li>
* <li>Supports null values but not null keys</li>
* </ul>
*
* <p>Implementation Details:</p>
* <ul>
* <li>Uses a HashMap for O(1) key-value lookups</li>
* <li>Maintains a doubly-linked list for tracking access order</li>
* <li>The head of the list contains the least recently used item</li>
* <li>The tail of the list contains the most recently used item</li>
* </ul>
*
* <p>Example usage:</p>
* <pre>
* LRUCache<String, Integer> cache = new LRUCache<>(3); // Create cache with capacity 3
* cache.put("A", 1); // Cache: A=1
* cache.put("B", 2); // Cache: A=1, B=2
* cache.put("C", 3); // Cache: A=1, B=2, C=3
* cache.get("A"); // Cache: B=2, C=3, A=1 (A moved to end)
* cache.put("D", 4); // Cache: C=3, A=1, D=4 (B evicted)
* </pre>
*
* @param <K> the type of keys maintained by this cache
* @param <V> the type of mapped values
*/
public class LRUCache<K, V> {

Expand All @@ -30,6 +55,11 @@ public LRUCache(int cap) {
setCapacity(cap);
}

/**
* Returns the current capacity of the cache.
*
* @param newCapacity the new capacity of the cache
*/
private void setCapacity(int newCapacity) {
checkCapacity(newCapacity);
for (int i = data.size(); i > newCapacity; i--) {
Expand All @@ -39,6 +69,11 @@ private void setCapacity(int newCapacity) {
this.cap = newCapacity;
}

/**
* Evicts the least recently used item from the cache.
*
* @return the evicted entry
*/
private Entry<K, V> evict() {
if (head == null) {
throw new RuntimeException("cache cannot be empty!");
Expand All @@ -50,12 +85,25 @@ private Entry<K, V> evict() {
return evicted;
}

/**
* Checks if the capacity is valid.
*
* @param capacity the capacity to check
*/
private void checkCapacity(int capacity) {
if (capacity <= 0) {
throw new RuntimeException("capacity must greater than 0!");
}
}

/**
* Returns the value to which the specified key is mapped, or null if this cache contains no
* mapping for the key.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped, or null if this cache contains no
* mapping for the key
*/
public V get(K key) {
if (!data.containsKey(key)) {
return null;
Expand All @@ -65,6 +113,11 @@ public V get(K key) {
return entry.getValue();
}

/**
* Moves the specified entry to the end of the list.
*
* @param entry the entry to move
*/
private void moveNodeToLast(Entry<K, V> entry) {
if (tail == entry) {
return;
Expand All @@ -86,6 +139,12 @@ private void moveNodeToLast(Entry<K, V> entry) {
tail = entry;
}

/**
* Associates the specified value with the specified key in this cache.
*
* @param key the key with which the specified value is to be associated
* @param value the value to be associated with the specified key
*/
public void put(K key, V value) {
if (data.containsKey(key)) {
final Entry<K, V> existingEntry = data.get(key);
Expand All @@ -107,6 +166,11 @@ public void put(K key, V value) {
data.put(key, newEntry);
}

/**
* Adds a new entry to the end of the list.
*
* @param newEntry the entry to add
*/
private void addNewEntry(Entry<K, V> newEntry) {
if (data.isEmpty()) {
head = newEntry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,147 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class LRUCacheTest {

private static final int SIZE = 5;
private LRUCache<Integer, Integer> cache;

@BeforeEach
void setUp() {
cache = new LRUCache<>(SIZE);
}

@Test
public void putAndGetIntegerValues() {
LRUCache<Integer, Integer> lruCache = new LRUCache<>(SIZE);
public void testBasicOperations() {
cache.put(1, 100);
assertEquals(100, cache.get(1));
assertNull(cache.get(2));
}

@Test
public void testEvictionPolicy() {
// Fill cache to capacity
for (int i = 0; i < SIZE; i++) {
lruCache.put(i, i);
cache.put(i, i * 100);
}

// Verify all elements are present
for (int i = 0; i < SIZE; i++) {
assertEquals(i, lruCache.get(i));
assertEquals(i * 100, cache.get(i));
}

// Add one more element, causing eviction of least recently used
cache.put(SIZE, SIZE * 100);

// First element should be evicted
assertNull(cache.get(0));
assertEquals(SIZE * 100, cache.get(SIZE));
}

@Test
public void putAndGetStringValues() {
LRUCache<String, String> lruCache = new LRUCache<>(SIZE);

public void testAccessOrder() {
// Fill cache
for (int i = 0; i < SIZE; i++) {
lruCache.put("key" + i, "value" + i);
cache.put(i, i);
}

for (int i = 0; i < SIZE; i++) {
assertEquals("value" + i, lruCache.get("key" + i));
}
// Access first element, making it most recently used
cache.get(0);

// Add new element, should evict second element (1)
cache.put(SIZE, SIZE);

assertEquals(0, cache.get(0)); // Should still exist
assertNull(cache.get(1)); // Should be evicted
assertEquals(SIZE, cache.get(SIZE)); // Should exist
}

@Test
public void testUpdateExistingKey() {
cache.put(1, 100);
assertEquals(100, cache.get(1));

// Update existing key
cache.put(1, 200);
assertEquals(200, cache.get(1));
}

@Test
public void nullKeysAndValues() {
LRUCache<Integer, Integer> mruCache = new LRUCache<>(SIZE);
mruCache.put(null, 2);
mruCache.put(6, null);
public void testNullValues() {
cache.put(1, null);
assertNull(cache.get(1));

assertEquals(2, mruCache.get(null));
assertNull(mruCache.get(6));
// Update null to non-null
cache.put(1, 100);
assertEquals(100, cache.get(1));

// Update non-null to null
cache.put(1, null);
assertNull(cache.get(1));
}

@Test
public void testStringKeysAndValues() {
LRUCache<String, String> stringCache = new LRUCache<>(SIZE);

stringCache.put("key1", "value1");
stringCache.put("key2", "value2");

assertEquals("value1", stringCache.get("key1"));
assertEquals("value2", stringCache.get("key2"));
}

@Test
public void testLongSequenceOfOperations() {
// Add elements beyond capacity multiple times
for (int i = 0; i < SIZE * 3; i++) {
cache.put(i, i * 100);

// Verify only the last SIZE elements are present
for (int j = Math.max(0, i - SIZE + 1); j <= i; j++) {
assertEquals(j * 100, cache.get(j));
}

// Verify elements before the window are evicted
if (i >= SIZE) {
assertNull(cache.get(i - SIZE));
}
}
}

@Test
public void overCapacity() {
LRUCache<Integer, Integer> mruCache = new LRUCache<>(SIZE);
void testCustomObjects() {
class TestObject {
private final String value;

for (int i = 0; i < 10; i++) {
mruCache.put(i, i);
TestObject(String value) {
this.value = value;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof TestObject) {
return value.equals(((TestObject) obj).value);
}
return false;
}

@Override
public int hashCode() {
return value == null ? 0 : value.hashCode();
}
}

assertEquals(9, mruCache.get(9));
LRUCache<Integer, TestObject> objectCache = new LRUCache<>(SIZE);
TestObject obj1 = new TestObject("test1");
TestObject obj2 = new TestObject("test2");

objectCache.put(1, obj1);
objectCache.put(2, obj2);

assertEquals(obj1, objectCache.get(1));
assertEquals(obj2, objectCache.get(2));
}
}