Skip to content

Commit b64e53c

Browse files
authored
Enhance docs, add more tests in LRUCache (#5950)
1 parent 520e464 commit b64e53c

File tree

2 files changed

+186
-31
lines changed

2 files changed

+186
-31
lines changed

src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java

+72-8
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,40 @@
44
import java.util.Map;
55

66
/**
7-
* Least recently used (LRU)
8-
* <p>
9-
* Discards the least recently used items first. This algorithm requires keeping
10-
* track of what was used when, which is expensive if one wants to make sure the
11-
* algorithm always discards the least recently used item.
12-
* https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
7+
* A Least Recently Used (LRU) Cache implementation.
138
*
14-
* @param <K> key type
15-
* @param <V> value type
9+
* <p>An LRU cache is a fixed-size cache that maintains items in order of use. When the cache reaches
10+
* its capacity and a new item needs to be added, it removes the least recently used item first.
11+
* This implementation provides O(1) time complexity for both get and put operations.</p>
12+
*
13+
* <p>Features:</p>
14+
* <ul>
15+
* <li>Fixed-size cache with configurable capacity</li>
16+
* <li>Constant time O(1) operations for get and put</li>
17+
* <li>Thread-unsafe - should be externally synchronized if used in concurrent environments</li>
18+
* <li>Supports null values but not null keys</li>
19+
* </ul>
20+
*
21+
* <p>Implementation Details:</p>
22+
* <ul>
23+
* <li>Uses a HashMap for O(1) key-value lookups</li>
24+
* <li>Maintains a doubly-linked list for tracking access order</li>
25+
* <li>The head of the list contains the least recently used item</li>
26+
* <li>The tail of the list contains the most recently used item</li>
27+
* </ul>
28+
*
29+
* <p>Example usage:</p>
30+
* <pre>
31+
* LRUCache<String, Integer> cache = new LRUCache<>(3); // Create cache with capacity 3
32+
* cache.put("A", 1); // Cache: A=1
33+
* cache.put("B", 2); // Cache: A=1, B=2
34+
* cache.put("C", 3); // Cache: A=1, B=2, C=3
35+
* cache.get("A"); // Cache: B=2, C=3, A=1 (A moved to end)
36+
* cache.put("D", 4); // Cache: C=3, A=1, D=4 (B evicted)
37+
* </pre>
38+
*
39+
* @param <K> the type of keys maintained by this cache
40+
* @param <V> the type of mapped values
1641
*/
1742
public class LRUCache<K, V> {
1843

@@ -30,6 +55,11 @@ public LRUCache(int cap) {
3055
setCapacity(cap);
3156
}
3257

58+
/**
59+
* Returns the current capacity of the cache.
60+
*
61+
* @param newCapacity the new capacity of the cache
62+
*/
3363
private void setCapacity(int newCapacity) {
3464
checkCapacity(newCapacity);
3565
for (int i = data.size(); i > newCapacity; i--) {
@@ -39,6 +69,11 @@ private void setCapacity(int newCapacity) {
3969
this.cap = newCapacity;
4070
}
4171

72+
/**
73+
* Evicts the least recently used item from the cache.
74+
*
75+
* @return the evicted entry
76+
*/
4277
private Entry<K, V> evict() {
4378
if (head == null) {
4479
throw new RuntimeException("cache cannot be empty!");
@@ -50,12 +85,25 @@ private Entry<K, V> evict() {
5085
return evicted;
5186
}
5287

88+
/**
89+
* Checks if the capacity is valid.
90+
*
91+
* @param capacity the capacity to check
92+
*/
5393
private void checkCapacity(int capacity) {
5494
if (capacity <= 0) {
5595
throw new RuntimeException("capacity must greater than 0!");
5696
}
5797
}
5898

99+
/**
100+
* Returns the value to which the specified key is mapped, or null if this cache contains no
101+
* mapping for the key.
102+
*
103+
* @param key the key whose associated value is to be returned
104+
* @return the value to which the specified key is mapped, or null if this cache contains no
105+
* mapping for the key
106+
*/
59107
public V get(K key) {
60108
if (!data.containsKey(key)) {
61109
return null;
@@ -65,6 +113,11 @@ public V get(K key) {
65113
return entry.getValue();
66114
}
67115

116+
/**
117+
* Moves the specified entry to the end of the list.
118+
*
119+
* @param entry the entry to move
120+
*/
68121
private void moveNodeToLast(Entry<K, V> entry) {
69122
if (tail == entry) {
70123
return;
@@ -86,6 +139,12 @@ private void moveNodeToLast(Entry<K, V> entry) {
86139
tail = entry;
87140
}
88141

142+
/**
143+
* Associates the specified value with the specified key in this cache.
144+
*
145+
* @param key the key with which the specified value is to be associated
146+
* @param value the value to be associated with the specified key
147+
*/
89148
public void put(K key, V value) {
90149
if (data.containsKey(key)) {
91150
final Entry<K, V> existingEntry = data.get(key);
@@ -107,6 +166,11 @@ public void put(K key, V value) {
107166
data.put(key, newEntry);
108167
}
109168

169+
/**
170+
* Adds a new entry to the end of the list.
171+
*
172+
* @param newEntry the entry to add
173+
*/
110174
private void addNewEntry(Entry<K, V> newEntry) {
111175
if (data.isEmpty()) {
112176
head = newEntry;

src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java

+114-23
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,147 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertNull;
55

6+
import org.junit.jupiter.api.BeforeEach;
67
import org.junit.jupiter.api.Test;
78

89
public class LRUCacheTest {
9-
1010
private static final int SIZE = 5;
11+
private LRUCache<Integer, Integer> cache;
12+
13+
@BeforeEach
14+
void setUp() {
15+
cache = new LRUCache<>(SIZE);
16+
}
1117

1218
@Test
13-
public void putAndGetIntegerValues() {
14-
LRUCache<Integer, Integer> lruCache = new LRUCache<>(SIZE);
19+
public void testBasicOperations() {
20+
cache.put(1, 100);
21+
assertEquals(100, cache.get(1));
22+
assertNull(cache.get(2));
23+
}
1524

25+
@Test
26+
public void testEvictionPolicy() {
27+
// Fill cache to capacity
1628
for (int i = 0; i < SIZE; i++) {
17-
lruCache.put(i, i);
29+
cache.put(i, i * 100);
1830
}
1931

32+
// Verify all elements are present
2033
for (int i = 0; i < SIZE; i++) {
21-
assertEquals(i, lruCache.get(i));
34+
assertEquals(i * 100, cache.get(i));
2235
}
36+
37+
// Add one more element, causing eviction of least recently used
38+
cache.put(SIZE, SIZE * 100);
39+
40+
// First element should be evicted
41+
assertNull(cache.get(0));
42+
assertEquals(SIZE * 100, cache.get(SIZE));
2343
}
2444

2545
@Test
26-
public void putAndGetStringValues() {
27-
LRUCache<String, String> lruCache = new LRUCache<>(SIZE);
28-
46+
public void testAccessOrder() {
47+
// Fill cache
2948
for (int i = 0; i < SIZE; i++) {
30-
lruCache.put("key" + i, "value" + i);
49+
cache.put(i, i);
3150
}
3251

33-
for (int i = 0; i < SIZE; i++) {
34-
assertEquals("value" + i, lruCache.get("key" + i));
35-
}
52+
// Access first element, making it most recently used
53+
cache.get(0);
54+
55+
// Add new element, should evict second element (1)
56+
cache.put(SIZE, SIZE);
57+
58+
assertEquals(0, cache.get(0)); // Should still exist
59+
assertNull(cache.get(1)); // Should be evicted
60+
assertEquals(SIZE, cache.get(SIZE)); // Should exist
61+
}
62+
63+
@Test
64+
public void testUpdateExistingKey() {
65+
cache.put(1, 100);
66+
assertEquals(100, cache.get(1));
67+
68+
// Update existing key
69+
cache.put(1, 200);
70+
assertEquals(200, cache.get(1));
3671
}
3772

3873
@Test
39-
public void nullKeysAndValues() {
40-
LRUCache<Integer, Integer> mruCache = new LRUCache<>(SIZE);
41-
mruCache.put(null, 2);
42-
mruCache.put(6, null);
74+
public void testNullValues() {
75+
cache.put(1, null);
76+
assertNull(cache.get(1));
4377

44-
assertEquals(2, mruCache.get(null));
45-
assertNull(mruCache.get(6));
78+
// Update null to non-null
79+
cache.put(1, 100);
80+
assertEquals(100, cache.get(1));
81+
82+
// Update non-null to null
83+
cache.put(1, null);
84+
assertNull(cache.get(1));
85+
}
86+
87+
@Test
88+
public void testStringKeysAndValues() {
89+
LRUCache<String, String> stringCache = new LRUCache<>(SIZE);
90+
91+
stringCache.put("key1", "value1");
92+
stringCache.put("key2", "value2");
93+
94+
assertEquals("value1", stringCache.get("key1"));
95+
assertEquals("value2", stringCache.get("key2"));
96+
}
97+
98+
@Test
99+
public void testLongSequenceOfOperations() {
100+
// Add elements beyond capacity multiple times
101+
for (int i = 0; i < SIZE * 3; i++) {
102+
cache.put(i, i * 100);
103+
104+
// Verify only the last SIZE elements are present
105+
for (int j = Math.max(0, i - SIZE + 1); j <= i; j++) {
106+
assertEquals(j * 100, cache.get(j));
107+
}
108+
109+
// Verify elements before the window are evicted
110+
if (i >= SIZE) {
111+
assertNull(cache.get(i - SIZE));
112+
}
113+
}
46114
}
47115

48116
@Test
49-
public void overCapacity() {
50-
LRUCache<Integer, Integer> mruCache = new LRUCache<>(SIZE);
117+
void testCustomObjects() {
118+
class TestObject {
119+
private final String value;
51120

52-
for (int i = 0; i < 10; i++) {
53-
mruCache.put(i, i);
121+
TestObject(String value) {
122+
this.value = value;
123+
}
124+
125+
@Override
126+
public boolean equals(Object obj) {
127+
if (obj instanceof TestObject) {
128+
return value.equals(((TestObject) obj).value);
129+
}
130+
return false;
131+
}
132+
133+
@Override
134+
public int hashCode() {
135+
return value == null ? 0 : value.hashCode();
136+
}
54137
}
55138

56-
assertEquals(9, mruCache.get(9));
139+
LRUCache<Integer, TestObject> objectCache = new LRUCache<>(SIZE);
140+
TestObject obj1 = new TestObject("test1");
141+
TestObject obj2 = new TestObject("test2");
142+
143+
objectCache.put(1, obj1);
144+
objectCache.put(2, obj2);
145+
146+
assertEquals(obj1, objectCache.get(1));
147+
assertEquals(obj2, objectCache.get(2));
57148
}
58149
}

0 commit comments

Comments
 (0)