-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
286: Practice Exercise: Building a Simple Cache with LRU Eviction
A few years ago, I was reviewing a PR from a developer who had implemented a simple HashMap to cache expensive database queries for a user dashboard. It looked clean and it worked perfectly in staging. But the moment it hit production, the server started crashing every few hours with an OutOfMemoryError. The problem was simple: the cache had no expiration or size limit. It just grew indefinitely as more users logged in, eating up the heap until the JVM finally gave up. We needed a way to keep the "hot" data but toss out the stuff no one had touched in an hour.
That's where a Least Recently Used (LRU) cache comes in. The core philosophy is that data accessed recently is more likely to be accessed again soon. When the cache hits its maximum capacity, instead of crashing the system, we evict the entry that has sat untouched for the longest period of time. In Java, you could build this from scratch using a combination of a HashMap and a doubly-linked list, but that's a lot of boilerplate. Instead, we have a hidden gem in the standard library: LinkedHashMap.
Leveraging Access-Order in LinkedHashMap
By default, a LinkedHashMap maintains entries in the order they were inserted. That's useful, but not for an LRU cache. To make it work for us, we have to use a specific constructor that enables "access-order." When this is turned on, every time you call get() or put() on an entry, Java internally moves that entry to the end of the list. The entry at the very front of the list becomes, by definition, the least recently used item.
// The 'true' argument here is the magic switch for access-order
Map<K, V> cache = new LinkedHashMap<>(capacity, 0.75f, true);
I've seen people try to manually re-insert items to move them to the end, but that's inefficient and error-prone. Let the JDK handle the pointer manipulation for you.
Automating Eviction with removeEldestEntry
Now we have a list that keeps the oldest items at the front, but LinkedHashMap still doesn't know when to stop growing. To fix this, we override a single method: removeEldestEntry. This method is called by the internal put and putAll logic. If it returns true, the map automatically deletes the oldest entry before adding the new one.
It's a remarkably elegant pattern. You aren't manually scanning the map or running a background cleanup thread; the eviction happens as a side effect of adding new data. Here is the basic structure I usually use when I need a quick, thread-unsafe cache:
public class SimpleLRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
public SimpleLRUCache(int maxCapacity) {
// Initial capacity, load factor, and accessOrder = true
super(maxCapacity, 0.75f, true);
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCapacity;
}
}
One quick warning: LinkedHashMap is not thread-safe. If you're using this in a multi-threaded environment—like a Spring boot controller—you'll need to wrap it in Collections.synchronizedMap() or use a different concurrency strategy. For this exercise, though, we'll focus on the core LRU logic.
📋 Practical Task
Exercise: Implementing a Fixed-Size User Profile Cache with LRU Eviction
You are tasked with building a cache for a User Profile service. The system fetches profiles from a slow API, so you need to cache the most recently accessed UserProfile objects. However, to prevent memory leaks, the cache must never exceed 3 profiles.
Requirements:
- Create a
UserProfilePOJO with auserId(int) and ausername(String). - Implement a class
UserProfileCachethat extendsLinkedHashMap. - Configure the cache to use access-order.
- Override
removeEldestEntryso that the cache maintains a maximum size of 3. - In your
mainmethod, perform the following sequence to verify the LRU behavior:- Add User 1, User 2, and User 3.
- Access User 1 (this should make it "recently used").
- Add User 4 (this should trigger eviction of User 2, as it's now the least recently used).
- Print the contents of the cache to verify that User 1, User 3, and User 4 remain, while User 2 is gone.
There are no comments for now.