Custom Implementation of LinkedHashMap in Java
Preserving Order with Efficiency
Introduction
The LinkedHashMap is a powerful Java collection that combines the fast lookup of a HashMap with the ordering of a linked list.
While a HashMap makes no guarantees about the order of elements, a LinkedHashMap preserves either insertion order or access order (depending on configuration).
This is particularly useful when order matters, for example:
- Maintaining a cache with least-recently-used eviction.
- Preserving configuration settings in the order they were loaded.
In this blog, we will:
- Understand the internal structure of LinkedHashMap.
- Learn about its ordering mechanism.
- Build a custom LinkedHashMap step-by-step.
- Compare it with HashMap.
- Discuss pros, cons, and use cases.
Internal Working of LinkedHashMap
LinkedHashMap extends HashMap and adds a doubly-linked list running through all entries.
This allows it to maintain order while still having constant-time performance for operations.
Structure
Hash Table (array of buckets) + Doubly Linked List
Each entry contains:
- Key
- Value
- Hash code
- Next (for bucket chaining)
- Before and After pointers (for linked list order)
This linked list connects entries in the order they were inserted (or accessed).
Two Ordering Modes
- Insertion order — entries remain in the order they were inserted. (Default mode)
- Access order — entries are moved to the end of the list when accessed.
Designing a Custom LinkedHashMap
We will implement a simplified version of LinkedHashMap that preserves insertion order.
Step 1: Define the Entry Class
This will hold the key-value pair along with pointers for linked list order.
class LinkedEntry {
K key;
V value;
LinkedEntry next; // for bucket chaining
LinkedEntry before; // previous entry in linked list
LinkedEntry after; // next entry in linked list
public LinkedEntry(K key, V value, LinkedEntry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
Step 2: Create the CustomLinkedHashMap Class
public class CustomLinkedHashMap {
private int capacity = 16;
private LinkedEntry[] table;
private LinkedEntry head; // start of linked list
private LinkedEntry tail; // end of linked list
public CustomLinkedHashMap() {
table = new LinkedEntry[capacity];
}
}
Step 3: Implement the put() Method
The put method will:
- Compute the bucket index using hashCode.
- Add the new entry at the head of the bucket chain (hashing).
- Add the entry at the end of the linked list (insertion order).
public void put(K key, V value) {
int hash = Math.abs(key.hashCode()) % capacity;
LinkedEntry newEntry = new LinkedEntry<>(key, value, null);
if (table[hash] == null) {
table[hash] = newEntry;
} else {
LinkedEntry current = table[hash];
LinkedEntry prev = null;
while (current != null) {
if (current.key.equals(key)) {
current.value = value; // update value
return;
}
prev = current;
current = current.next;
}
prev.next = newEntry;
}
// Maintain insertion order
if (head == null) {
head = tail = newEntry;
} else {
tail.after = newEntry;
newEntry.before = tail;
tail = newEntry;
}
}
Step 4: Implement the get() Method
Retrieve value by key without disturbing order.
public V get(K key) {
int hash = Math.abs(key.hashCode()) % capacity;
LinkedEntry current = table[hash];
while (current != null) {
if (current.key.equals(key)) {
return current.value;
}
current = current.next;
}
return null;
}
Step 5: Implement the remove() Method
Remove entry from both the bucket chain and the linked list.
public boolean remove(K key) {
int hash = Math.abs(key.hashCode()) % capacity;
LinkedEntry current = table[hash];
LinkedEntry prev = null;
while (current != null) {
if (current.key.equals(key)) {
if (prev == null) {
table[hash] = current.next;
} else {
prev.next = current.next;
}
// Remove from linked list
if (current.before != null) {
current.before.after = current.after;
} else {
head = current.after;
}
if (current.after != null) {
current.after.before = current.before;
} else {
tail = current.before;
}
return true;
}
prev = current;
current = current.next;
}
return false;
}
Step 6: Implement display() Method
Display all entries in insertion order.
public void display() {
LinkedEntry current = head;
while (current != null) {
System.out.print("[" + current.key + "=" + current.value + "] ");
current = current.after;
}
System.out.println();
}
Step 7: Testing CustomLinkedHashMap
public class TestCustomLinkedHashMap {
public static void main(String[] args) {
CustomLinkedHashMap map = new CustomLinkedHashMap<>();
map.put(1, "Java");
map.put(2, "Python");
map.put(3, "C++");
map.put(17, "Go"); // collision with key=1
map.display();
System.out.println("Get key 2: " + map.get(2));
map.remove(1);
map.display();
}
}
Expected Output
[1=Java] [2=Python] [3=C++] [17=Go]
Get key 2: Python
[2=Python] [3=C++] [17=Go]
This shows that insertion order is preserved even after deletions.
Custom LinkedHashMap Implementation
// Custom LinkedHashMap Implementation
// Preserves insertion order like java.util.LinkedHashMap
class LinkedEntry<K, V> {
K key;
V value;
LinkedEntry<K, V> next; // For bucket chaining (hash collisions)
LinkedEntry<K, V> before; // Previous entry in insertion order
LinkedEntry<K, V> after; // Next entry in insertion order
public LinkedEntry(K key, V value, LinkedEntry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
public class CustomLinkedHashMap<K, V> {
private int capacity = 16;
private LinkedEntry<K, V>[] table;
private LinkedEntry<K, V> head; // Start of insertion order
private LinkedEntry<K, V> tail; // End of insertion order
@SuppressWarnings("unchecked")
public CustomLinkedHashMap() {
table = new LinkedEntry[capacity];
}
// Step 1: Put method
public void put(K key, V value) {
int hash = Math.abs(key.hashCode()) % capacity;
LinkedEntry<K, V> newEntry = new LinkedEntry<>(key, value, null);
if (table[hash] == null) {
table[hash] = newEntry;
} else {
LinkedEntry<K, V> current = table[hash];
LinkedEntry<K, V> prev = null;
while (current != null) {
if (current.key.equals(key)) {
current.value = value; // update existing key
return;
}
prev = current;
current = current.next;
}
prev.next = newEntry;
}
// Maintain insertion order
if (head == null) {
head = tail = newEntry;
} else {
tail.after = newEntry;
newEntry.before = tail;
tail = newEntry;
}
}
// Step 2: Get method
public V get(K key) {
int hash = Math.abs(key.hashCode()) % capacity;
LinkedEntry<K, V> current = table[hash];
while (current != null) {
if (current.key.equals(key)) {
return current.value;
}
current = current.next;
}
return null;
}
// Step 3: Remove method
public boolean remove(K key) {
int hash = Math.abs(key.hashCode()) % capacity;
LinkedEntry<K, V> current = table[hash];
LinkedEntry<K, V> prev = null;
while (current != null) {
if (current.key.equals(key)) {
// Remove from hash bucket
if (prev == null) {
table[hash] = current.next;
} else {
prev.next = current.next;
}
// Remove from linked list (insertion order)
if (current.before != null) {
current.before.after = current.after;
} else {
head = current.after;
}
if (current.after != null) {
current.after.before = current.before;
} else {
tail = current.before;
}
return true;
}
prev = current;
current = current.next;
}
return false;
}
// Step 4: Display method — prints in insertion order
public void display() {
LinkedEntry<K, V> current = head;
while (current != null) {
System.out.print("[" + current.key + "=" + current.value + "] ");
current = current.after;
}
System.out.println();
}
}
// Step 5: Test the CustomLinkedHashMap
class TestCustomLinkedHashMap {
public static void main(String[] args) {
CustomLinkedHashMap<Integer, String> map = new CustomLinkedHashMap<>();
map.put(1, "Java");
map.put(2, "Python");
map.put(3, "C++");
map.put(17, "Go"); // Collision with key=1 (same bucket likely)
map.display();
System.out.println("Get key 2: " + map.get(2));
map.remove(1);
map.display();
}
}
Expected Output
[1=Java] [2=Python] [3=C++] [17=Go]
Get key 2: Python
[2=Python] [3=C++] [17=Go]
Advantages of LinkedHashMap
- Maintains insertion or access order.
- Provides predictable iteration order.
- Almost the same performance as HashMap.
- Useful in caching scenarios.
Disadvantages of LinkedHashMap
- Slightly more memory usage due to linked list pointers.
- Slightly slower than HashMap due to maintaining order.
- Not synchronized (use Collections.synchronizedMap() or ConcurrentHashMap for thread safety).
Real-World Applications
- Implementing LRU Caches.
- Maintaining order-sensitive data, e.g., configuration settings.
- Building index-based systems where order matters.
Conclusion
In this blog, we created a CustomLinkedHashMap to understand how Java’s LinkedHashMap preserves order while maintaining efficient access.
We learned:
- How it combines a hash table with a doubly-linked list.
- How insertion and deletion maintain order.
- The internal mechanics behind one of Java’s most versatile data structures.
Next Blog- Custom Implementation of LinkedHashSet in Java