mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-07-05 16:27:33 +08:00
Enhance docs, add more tests in MRUCache
(#5951)
This commit is contained in:
@ -4,14 +4,17 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Most recently used (MRU)
|
* Represents a Most Recently Used (MRU) Cache.
|
||||||
* <p>
|
* <p>
|
||||||
* In contrast to Least Recently Used (LRU), MRU discards the most recently used
|
* In contrast to the Least Recently Used (LRU) strategy, the MRU caching policy
|
||||||
* items first.
|
* evicts the most recently accessed items first. This class provides methods to
|
||||||
* https://en.wikipedia.org/wiki/Cache_replacement_policies#Most_recently_used_(MRU)
|
* store key-value pairs and manage cache eviction based on this policy.
|
||||||
*
|
*
|
||||||
* @param <K> key type
|
* For more information, refer to:
|
||||||
* @param <V> value type
|
* <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> {
|
public class MRUCache<K, V> {
|
||||||
|
|
||||||
@ -21,40 +24,74 @@ public class MRUCache<K, V> {
|
|||||||
private int cap;
|
private int cap;
|
||||||
private static final int DEFAULT_CAP = 100;
|
private static final int DEFAULT_CAP = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an MRUCache with the default capacity.
|
||||||
|
*/
|
||||||
public MRUCache() {
|
public MRUCache() {
|
||||||
setCapacity(DEFAULT_CAP);
|
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) {
|
private void setCapacity(int newCapacity) {
|
||||||
checkCapacity(newCapacity);
|
checkCapacity(newCapacity);
|
||||||
for (int i = data.size(); i > newCapacity; i--) {
|
while (data.size() > newCapacity) {
|
||||||
Entry<K, V> evicted = evict();
|
Entry<K, V> evicted = evict();
|
||||||
data.remove(evicted.getKey());
|
data.remove(evicted.getKey());
|
||||||
}
|
}
|
||||||
this.cap = newCapacity;
|
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) {
|
private void checkCapacity(int capacity) {
|
||||||
if (capacity <= 0) {
|
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() {
|
private Entry<K, V> evict() {
|
||||||
if (head == null) {
|
if (head == null) {
|
||||||
throw new RuntimeException("cache cannot be empty!");
|
throw new RuntimeException("Cache cannot be empty!");
|
||||||
}
|
}
|
||||||
final Entry<K, V> evicted = this.tail;
|
final Entry<K, V> evicted = this.tail;
|
||||||
tail = evicted.getPreEntry();
|
tail = evicted.getPreEntry();
|
||||||
tail.setNextEntry(null);
|
if (tail != null) {
|
||||||
|
tail.setNextEntry(null);
|
||||||
|
}
|
||||||
evicted.setNextEntry(null);
|
evicted.setNextEntry(null);
|
||||||
return evicted;
|
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) {
|
public V get(K key) {
|
||||||
if (!data.containsKey(key)) {
|
if (!data.containsKey(key)) {
|
||||||
return null;
|
return null;
|
||||||
@ -64,11 +101,19 @@ public class MRUCache<K, V> {
|
|||||||
return entry.getValue();
|
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) {
|
public void put(K key, V value) {
|
||||||
if (data.containsKey(key)) {
|
if (data.containsKey(key)) {
|
||||||
final Entry<K, V> exitingEntry = data.get(key);
|
final Entry<K, V> existingEntry = data.get(key);
|
||||||
exitingEntry.setValue(value);
|
existingEntry.setValue(value);
|
||||||
moveEntryToLast(exitingEntry);
|
moveEntryToLast(existingEntry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Entry<K, V> newEntry;
|
Entry<K, V> newEntry;
|
||||||
@ -84,6 +129,11 @@ public class MRUCache<K, V> {
|
|||||||
data.put(key, newEntry);
|
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) {
|
private void addNewEntry(Entry<K, V> newEntry) {
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
head = newEntry;
|
head = newEntry;
|
||||||
@ -96,6 +146,11 @@ public class MRUCache<K, V> {
|
|||||||
tail = 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) {
|
private void moveEntryToLast(Entry<K, V> entry) {
|
||||||
if (tail == entry) {
|
if (tail == entry) {
|
||||||
return;
|
return;
|
||||||
@ -117,8 +172,14 @@ public class MRUCache<K, V> {
|
|||||||
tail = 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> {
|
static final class Entry<I, J> {
|
||||||
|
|
||||||
private Entry<I, J> preEntry;
|
private Entry<I, J> preEntry;
|
||||||
private Entry<I, J> nextEntry;
|
private Entry<I, J> nextEntry;
|
||||||
private I key;
|
private I key;
|
||||||
|
@ -11,27 +11,27 @@ public class MRUCacheTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void putAndGetIntegerValues() {
|
public void putAndGetIntegerValues() {
|
||||||
MRUCache<Integer, Integer> lruCache = new MRUCache<>(SIZE);
|
MRUCache<Integer, Integer> mruCache = new MRUCache<>(SIZE);
|
||||||
|
|
||||||
for (int i = 0; i < SIZE; i++) {
|
for (int i = 0; i < SIZE; i++) {
|
||||||
lruCache.put(i, i);
|
mruCache.put(i, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < SIZE; i++) {
|
for (int i = 0; i < SIZE; i++) {
|
||||||
assertEquals(i, lruCache.get(i));
|
assertEquals(i, mruCache.get(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void putAndGetStringValues() {
|
public void putAndGetStringValues() {
|
||||||
MRUCache<String, String> lruCache = new MRUCache<>(SIZE);
|
MRUCache<String, String> mruCache = new MRUCache<>(SIZE);
|
||||||
|
|
||||||
for (int i = 0; i < SIZE; i++) {
|
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++) {
|
for (int i = 0; i < SIZE; i++) {
|
||||||
assertEquals("value" + i, lruCache.get("key" + i));
|
assertEquals("value" + i, mruCache.get("key" + i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +53,73 @@ public class MRUCacheTest {
|
|||||||
mruCache.put(i, i);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user