Skip to content

refactor: Enhance docs, add more tests in MRUCache #5951

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 9 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,14 +4,17 @@
import java.util.Map;

/**
* Most recently used (MRU)
* Represents a Most Recently Used (MRU) Cache.
* <p>
* In contrast to Least Recently Used (LRU), MRU discards the most recently used
* items first.
* https://en.wikipedia.org/wiki/Cache_replacement_policies#Most_recently_used_(MRU)
* In contrast to the Least Recently Used (LRU) strategy, the MRU caching policy
* evicts the most recently accessed items first. This class provides methods to
* store key-value pairs and manage cache eviction based on this policy.
*
* @param <K> key type
* @param <V> value type
* For more information, refer to:
* <a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Most_recently_used_(MRU)">MRU on Wikipedia</a>.
*
* @param <K> the type of keys maintained by this cache
* @param <V> the type of values associated with the keys
*/
public class MRUCache<K, V> {

Expand All @@ -21,40 +24,74 @@ public class MRUCache<K, V> {
private int cap;
private static final int DEFAULT_CAP = 100;

/**
* Creates an MRUCache with the default capacity.
*/
public MRUCache() {
setCapacity(DEFAULT_CAP);
}

/**
* Creates an MRUCache with a specified capacity.
*
* @param cap the maximum number of items the cache can hold
*/
public MRUCache(int cap) {
setCapacity(cap);
}

/**
* Sets the capacity of the cache and evicts items if the new capacity
* is less than the current number of items.
*
* @param newCapacity the new capacity to set
*/
private void setCapacity(int newCapacity) {
checkCapacity(newCapacity);
for (int i = data.size(); i > newCapacity; i--) {
while (data.size() > newCapacity) {
Entry<K, V> evicted = evict();
data.remove(evicted.getKey());
}
this.cap = newCapacity;
}

/**
* Checks if the specified capacity is valid.
*
* @param capacity the capacity to check
* @throws IllegalArgumentException if the capacity is less than or equal to zero
*/
private void checkCapacity(int capacity) {
if (capacity <= 0) {
throw new RuntimeException("capacity must greater than 0!");
throw new IllegalArgumentException("Capacity must be greater than 0!");
}
}

/**
* Evicts the most recently used entry from the cache.
*
* @return the evicted entry
* @throws RuntimeException if the cache is empty
*/
private Entry<K, V> evict() {
if (head == null) {
throw new RuntimeException("cache cannot be empty!");
throw new RuntimeException("Cache cannot be empty!");
}
final Entry<K, V> evicted = this.tail;
tail = evicted.getPreEntry();
tail.setNextEntry(null);
if (tail != null) {
tail.setNextEntry(null);
}
evicted.setNextEntry(null);
return evicted;
}

public MRUCache(int cap) {
setCapacity(cap);
}

/**
* Retrieves the value associated with the specified key.
*
* @param key the key whose associated value is to be returned
* @return the value associated with the specified key, or null if the key does not exist
*/
public V get(K key) {
if (!data.containsKey(key)) {
return null;
Expand All @@ -64,11 +101,19 @@ public V get(K key) {
return entry.getValue();
}

/**
* Associates the specified value with the specified key in the cache.
* If the key already exists, its value is updated and the entry is moved to the most recently used position.
* If the cache is full, the most recently used entry is evicted before adding the new entry.
*
* @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> exitingEntry = data.get(key);
exitingEntry.setValue(value);
moveEntryToLast(exitingEntry);
final Entry<K, V> existingEntry = data.get(key);
existingEntry.setValue(value);
moveEntryToLast(existingEntry);
return;
}
Entry<K, V> newEntry;
Expand All @@ -84,6 +129,11 @@ public void put(K key, V value) {
data.put(key, newEntry);
}

/**
* Adds a new entry to the cache and updates the head and tail pointers accordingly.
*
* @param newEntry the new entry to be added
*/
private void addNewEntry(Entry<K, V> newEntry) {
if (data.isEmpty()) {
head = newEntry;
Expand All @@ -96,6 +146,11 @@ private void addNewEntry(Entry<K, V> newEntry) {
tail = newEntry;
}

/**
* Moves the specified entry to the most recently used position in the cache.
*
* @param entry the entry to be moved
*/
private void moveEntryToLast(Entry<K, V> entry) {
if (tail == entry) {
return;
Expand All @@ -117,8 +172,14 @@ private void moveEntryToLast(Entry<K, V> entry) {
tail = entry;
}

/**
* A nested class representing an entry in the cache, which holds a key-value pair
* and references to the previous and next entries in the linked list structure.
*
* @param <I> the type of the key
* @param <J> the type of the value
*/
static final class Entry<I, J> {

private Entry<I, J> preEntry;
private Entry<I, J> nextEntry;
private I key;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ public class MRUCacheTest {

@Test
public void putAndGetIntegerValues() {
MRUCache<Integer, Integer> lruCache = new MRUCache<>(SIZE);
MRUCache<Integer, Integer> mruCache = new MRUCache<>(SIZE);

for (int i = 0; i < SIZE; i++) {
lruCache.put(i, i);
mruCache.put(i, i);
}

for (int i = 0; i < SIZE; i++) {
assertEquals(i, lruCache.get(i));
assertEquals(i, mruCache.get(i));
}
}

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

for (int i = 0; i < SIZE; i++) {
lruCache.put("key" + i, "value" + i);
mruCache.put("key" + i, "value" + i);
}

for (int i = 0; i < SIZE; i++) {
assertEquals("value" + i, lruCache.get("key" + i));
assertEquals("value" + i, mruCache.get("key" + i));
}
}

Expand All @@ -53,6 +53,73 @@ public void overCapacity() {
mruCache.put(i, i);
}

assertEquals(9, mruCache.get(9));
// After inserting 10 items, the cache should have evicted the least recently used ones.
assertEquals(9, mruCache.get(9)); // Most recently used
assertEquals(0, mruCache.get(0)); // Least recently used, should be evicted
}

@Test
public void overwriteExistingKey() {
MRUCache<Integer, String> mruCache = new MRUCache<>(SIZE);
mruCache.put(1, "one");
mruCache.put(1, "uno"); // Overwriting the value for key 1

assertEquals("uno", mruCache.get(1));
assertNull(mruCache.get(2)); // Ensure other keys are unaffected
}

@Test
public void evictionOrder() {
MRUCache<Integer, Integer> mruCache = new MRUCache<>(SIZE);

for (int i = 0; i < SIZE; i++) {
mruCache.put(i, i);
}

// Access a key to make it most recently used
mruCache.get(2);

// Add new items to trigger eviction
mruCache.put(5, 5);
mruCache.put(6, 6);

// Key 3 should be evicted since 2 is the most recently used
assertEquals(3, mruCache.get(3));
assertEquals(4, mruCache.get(4)); // Key 4 should still be available
assertEquals(6, mruCache.get(6)); // Key 6 should be available
}

@Test
public void cacheHandlesLargeValues() {
MRUCache<String, String> mruCache = new MRUCache<>(SIZE);

for (int i = 0; i < SIZE; i++) {
mruCache.put("key" + i, "value" + i);
}

// Verify values
for (int i = 0; i < SIZE; i++) {
assertEquals("value" + i, mruCache.get("key" + i));
}

// Add large value
mruCache.put("largeKey", "largeValue");

// Verify eviction of the least recently used (key 0 should be evicted)
assertEquals("value0", mruCache.get("key0"));
assertEquals("largeValue", mruCache.get("largeKey"));
}

@Test
public void testEmptyCacheBehavior() {
MRUCache<Integer, Integer> mruCache = new MRUCache<>(SIZE);

// Verify that accessing any key returns null
assertNull(mruCache.get(1));
assertNull(mruCache.get(100));

// Adding to cache and checking again
mruCache.put(1, 10);
assertEquals(10, mruCache.get(1));
}
}