Skip to content

Commit

Permalink
✨ feat: add LFUCache4j #5
Browse files Browse the repository at this point in the history
  • Loading branch information
pnguyen215 committed Jun 9, 2024
1 parent 2bd3a6f commit 467930c
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 0 deletions.
85 changes: 85 additions & 0 deletions docs/002_LFUCache4j.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# LFUCache4j

LFUCache4j is a thread-safe implementation of a Least Frequently Used (LFU) cache in Java. This cache supports generic
key-value pairs and ensures that the least frequently accessed elements are evicted first when the cache reaches its
capacity.

## Features

- **Thread-Safe**: Utilizes a ReentrantLock to ensure thread safety.
- **LFU Eviction Policy**: Evicts the least frequently used elements first.
- **Generic**: Supports generic types for keys and values.

## Usage

### Creating an LFU Cache

```java
LFUCache4j<Integer, String> cache = new LFUCache4j<>(2);
```

### Adding Elements to the Cache

```java
cache.put(1, "one");
cache.put(2, "two");
```

### Retrieving Elements from the Cache

```java
String value1 = cache.get(1); // Returns "one"
String value2 = cache.get(2); // Returns "two"
```

### Updating an Element in the Cache

```java
cache.put(1, "ONE"); // Updates the value associated with key 1
String updatedValue = cache.get(1); // Returns "ONE"
```

### Handling Cache Eviction

When the cache reaches its capacity, the least frequently used element is evicted to make space for new elements.

```java
cache.put(3, "three"); // Evicts the least frequently used element
String evictedValue = cache.get(2); // Returns null, as element with key 2 is evicted
```

## API Reference

### Constructor

- `LFUCache4j(int capacity)`: Creates a new LFU cache with the specified capacity.

### Methods

- `V get(K key)`: Retrieves the value associated with the specified key. Updates the access frequency of the key. Returns null if the key is not found.
- `void put(K key, V value)`: Inserts the specified key-value pair into the cache. If the cache is at capacity, the least frequently used item is evicted. If the key already exists, its value is updated and its frequency is incremented.

## Example

```java
public class Main {
public static void main(String[] args) {
LFUCache4j<Integer, String> cache = new LFUCache4j<>(2);

cache.put(1, "one");
cache.put(2, "two");

System.out.println(cache.get(1)); // Output: one
System.out.println(cache.get(2)); // Output: two

cache.put(3, "three"); // Evicts key 2

System.out.println(cache.get(2)); // Output: null
System.out.println(cache.get(3)); // Output: three

cache.put(1, "ONE"); // Updates value for key 1

System.out.println(cache.get(1)); // Output: ONE
}
}
```
105 changes: 105 additions & 0 deletions plugin/src/main/groovy/org/alpha4j/ds/LFUCache4j.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.alpha4j.ds;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
* LFUCache4j is a thread-safe implementation of the Least Frequently Used (LFU) cache.
* It supports generic key-value pairs and ensures that the least frequently accessed
* elements are evicted first when the cache reaches its capacity.
*
* @param <K> the type of keys maintained by this cache
* @param <V> the type of mapped values
*/
public class LFUCache4j<K, V> {
protected final ReentrantLock lock = new ReentrantLock(); // Define a lock to ensure thread safety
protected int capacity; // Cache capacity
protected int size = 0; // Current size of the cache
protected Map<K, V> cache; // Map to store keys and their corresponding values
protected Map<K, Integer> frequencies; // Map to store keys and their corresponding frequencies
protected Map<Integer, LinkedHashSet<K>> frequencyIndexes; // Map to store frequencies and the corresponding sets of keys
protected int minFrequency; // Variable to keep track of the minimum frequency

/**
* Constructor to initialize the LFUCache4j with a specific capacity.
*
* @param capacity the maximum number of items that can be held in the cache
*/
public LFUCache4j(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
this.frequencies = new HashMap<>();
this.frequencyIndexes = new HashMap<>();
this.minFrequency = 1;
}

/**
* Retrieves the value associated with the specified key from the cache.
* If the key is not found, returns null. This method also updates the
* frequency of the accessed 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 is not found
*/
@SuppressWarnings({"UnusedReturnValue"})
public V get(K key) {
try {
lock.lock();
if (!cache.containsKey(key)) {
return null;
}
int frequency = frequencies.get(key); // Get the current frequency of the key
frequencyIndexes.get(frequency).remove(key); // Remove the key from the current frequency list
// If the current frequency list is empty and the frequency equals minFrequency, increment minFrequency
if (frequencyIndexes.get(frequency).isEmpty() && frequency == minFrequency) {
minFrequency++;
}
frequency++; // Increment the frequency and update the data structures
frequencies.put(key, frequency);
frequencyIndexes.computeIfAbsent(frequency, k -> new LinkedHashSet<>()).add(key);
return cache.get(key);
} finally {
lock.unlock();
}
}

/**
* Inserts the specified key-value pair into the cache. If the cache is at capacity,
* the least frequently used item will be removed to make space for the new entry.
* If the key already exists, its value will be updated and its frequency will be incremented.
*
* @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) {
try {
lock.lock();
if (capacity <= 0) {
return;
}
if (cache.containsKey(key)) {
cache.put(key, value); // Update the value and increase the frequency
this.get(key); // Update the frequency by calling get
return;
}
if (size >= capacity) {
// Remove the least frequently used element
K evict = frequencyIndexes.get(minFrequency).iterator().next();
frequencyIndexes.get(minFrequency).remove(evict);
cache.remove(evict);
frequencies.remove(evict);
size--;
}
// Add the new key and value
cache.put(key, value);
frequencies.put(key, 1);
frequencyIndexes.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);
minFrequency = 1; // Reset minFrequency to 1 for the new entry
size++;
} finally {
lock.unlock();
}
}
}
119 changes: 119 additions & 0 deletions plugin/src/test/groovy/org/alpha4j/LFUCache4jTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.alpha4j;

import org.alpha4j.ds.LFUCache4j;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

public class LFUCache4jTest {

private LFUCache4j<Integer, String> cache;

@Before
public void setUp() {
// Initialize the cache with a capacity of 2
cache = new LFUCache4j<>(2);
}

@Test
public void testPutAndGet() {
// Test adding elements to the cache
cache.put(1, "one");
cache.put(2, "two");

// Verify the elements are correctly added
assertEquals("one", cache.get(1));
assertEquals("two", cache.get(2));

// Test updating an existing element
cache.put(1, "ONE");
assertEquals("ONE", cache.get(1));
}

@Test
public void testEviction() {
// Add elements to fill the cache
cache.put(1, "one");
cache.put(2, "two");

// Access element 1 to increase its frequency
cache.get(1);

// Add another element to trigger eviction
cache.put(3, "three");

// Verify that element 2 is evicted (as it was less frequently accessed)
assertNull(cache.get(2));
assertEquals("one", cache.get(1));
assertEquals("three", cache.get(3));
}

@Test
public void testFrequencyUpdate() {
// Add elements to the cache
cache.put(1, "one");
cache.put(2, "two");

// Access elements to update their frequencies
cache.get(1);
cache.get(1);
cache.get(2);

// Add another element to trigger eviction
cache.put(3, "three");

// Verify that element 2 is evicted (since element 1 has higher frequency)
assertNull(cache.get(2));
assertEquals("one", cache.get(1));
assertEquals("three", cache.get(3));
}

@Test
public void testUpdateValue() {
// Add an element to the cache
cache.put(1, "one");

// Verify the value is correctly added
assertEquals("one", cache.get(1));

// Update the value of the existing element
cache.put(1, "ONE");

// Verify the value is correctly updated
assertEquals("ONE", cache.get(1));
}

@Test
public void testMinFrequency() {
// Add elements to the cache
cache.put(1, "one");
cache.put(2, "two");

// Access elements to update their frequencies
cache.get(1);
cache.get(2);
cache.get(2);

// Add another element to trigger eviction
cache.put(3, "three");

// Verify that element 1 is evicted (since it has the lowest frequency)
assertNull(cache.get(1));
assertEquals("two", cache.get(2));
assertEquals("three", cache.get(3));
}

@Test
public void testZeroCapacity() {
// Initialize the cache with zero capacity
LFUCache4j<Integer, String> zeroCapacityCache = new LFUCache4j<>(0);

// Attempt to add elements to the zero-capacity cache
zeroCapacityCache.put(1, "one");

// Verify that no elements are added
assertNull(zeroCapacityCache.get(1));
}
}

0 comments on commit 467930c

Please sign in to comment.