For years, LinkedHashMap has been Java's go-to ordered Map implementation, cherished for its predictable iteration order. However, its API for accessing ends of the map has always been awkward—until now. With the introduction of JEP 431: Sequenced Collections in Java 21, LinkedHashMap gained a powerful new identity by implementing the SequencedMap interface, finally providing a clean, standardized way to interact with its ordering.
The Problem: The Missing Order API
Traditionally, if you wanted to access the "first" or "last" element of a LinkedHashMap, you had to resort to inefficient workarounds:
// The old, inefficient way
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
// ... add elements ...
// Get first entry - O(n) traversal!
Map.Entry<String, Integer> firstEntry = map.entrySet().iterator().next();
// Get last entry - even worse!
Map.Entry<String, Integer> lastEntry = null;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
lastEntry = entry;
}
// Remove first element - equally painful
if (!map.isEmpty()) {
Map.Entry<String, Integer> first = map.entrySet().iterator().next();
map.remove(first.getKey());
}
This was cumbersome, inefficient (O(n) for accessing ends), and error-prone.
The Solution: SequencedMap Interface
Java 21's SequencedMap interface provides a unified contract for maps that maintain a well-defined encounter order. LinkedHashMap now implements this interface, offering direct, O(1) access to both ends of the map.
The enhanced LinkedHashMap now provides a comprehensive set of ordered operations, as shown in the following table:
| Operation | Method | Description | Complexity |
|---|---|---|---|
| Get first element | firstEntry() | Returns first key-value mapping | O(1) |
| Get last element | lastEntry() | Returns last key-value mapping | O(1) |
| Get first key | firstKey() | Returns first key | O(1) |
| Get last key | lastKey() | Returns last key | O(1) |
| Add to front | putFirst(K, V) | Inserts mapping at beginning | O(1) |
| Add to end | putLast(K, V) | Inserts mapping at end | O(1) |
| Remove first | pollFirstEntry() | Removes and returns first entry | O(1) |
| Remove last | pollLastEntry() | Removes and returns last entry | O(1) |
| Reverse view | reversed() | Returns reverse-ordered view | O(1) |
Practical Usage Examples
1. Basic Sequenced Operations
// Create a LinkedHashMap - now a SequencedMap!
SequencedMap<String, Integer> scores = new LinkedHashMap<>();
// Add elements (maintains insertion order by default)
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
System.out.println(scores); // {Alice=95, Bob=87, Charlie=92}
// Access ends efficiently
Map.Entry<String, Integer> first = scores.firstEntry();
Map.Entry<String, Integer> last = scores.lastEntry();
System.out.println("First: " + first); // First: Alice=95
System.out.println("Last: " + last); // Last: Charlie=92
// Add elements at specific positions
scores.putFirst("Zoe", 99); // Add at beginning
scores.putLast("David", 88); // Add at end
System.out.println(scores); // {Zoe=99, Alice=95, Bob=87, Charlie=92, David=88}
// Remove from ends
Map.Entry<String, Integer> removedFirst = scores.pollFirstEntry();
Map.Entry<String, Integer> removedLast = scores.pollLastEntry();
System.out.println("Removed first: " + removedFirst); // Removed first: Zoe=99
System.out.println("Removed last: " + removedLast); // Removed last: David=88
System.out.println(scores); // {Alice=95, Bob=87, Charlie=92}
2. Reverse Iteration and Processing
SequencedMap<String, LocalDate> projectMilestones = new LinkedHashMap<>();
projectMilestones.put("Design", LocalDate.of(2024, 1, 15));
projectMilestones.put("Implementation", LocalDate.of(2024, 2, 28));
projectMilestones.put("Testing", LocalDate.of(2024, 3, 31));
// Process in chronological order (insertion order)
System.out.println("Chronological order:");
for (Map.Entry<String, LocalDate> entry : projectMilestones.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Process in reverse chronological order
System.out.println("\nReverse chronological order:");
SequencedMap<String, LocalDate> reversed = projectMilestones.reversed();
for (Map.Entry<String, LocalDate> entry : reversed.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Output:
// Chronological order:
// Design: 2024-01-15
// Implementation: 2024-02-28
// Testing: 2024-03-31
//
// Reverse chronological order:
// Testing: 2024-03-31
// Implementation: 2024-02-28
// Design: 2024-01-15
3. Building an LRU (Least Recently Used) Cache
LinkedHashMap has always been perfect for LRU caches, but now the API is even cleaner:
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LRUCache(int maxSize) {
// Access-order = true (LRU behavior), Insertion-order = false
super(maxSize, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
// New sequenced operations enhance the cache API
public Map.Entry<K, V> getLeastRecent() {
return this.firstEntry(); // O(1) access to least recent
}
public Map.Entry<K, V> getMostRecent() {
return this.lastEntry(); // O(1) access to most recent
}
}
// Usage
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("A", "Data A");
cache.put("B", "Data B");
cache.put("C", "Data C");
// Access pattern affects order
cache.get("A"); // Makes A most recently used
System.out.println("Least recent: " + cache.getLeastRecent()); // B=Data B
System.out.println("Most recent: " + cache.getMostRecent()); // A=Data A
// Adding new item evicts least recent
cache.put("D", "Data D");
System.out.println(cache); // {C=Data C, A=Data A, D=Data D} - B was evicted
Advanced Patterns and Considerations
1. Access Order vs Insertion Order
LinkedHashMap supports two ordering modes, and SequencedMap works with both:
// Insertion-order (default)
LinkedHashMap<String, Integer> insertionOrder = new LinkedHashMap<>();
insertionOrder.put("Z", 1);
insertionOrder.put("A", 2);
insertionOrder.put("M", 3);
// Order: Z, A, M (as inserted)
// Access-order (for LRU caches)
LinkedHashMap<String, Integer> accessOrder = new LinkedHashMap<>(16, 0.75f, true);
accessOrder.put("Z", 1);
accessOrder.put("A", 2);
accessOrder.put("M", 3);
accessOrder.get("Z"); // Access moves Z to end
// Order: A, M, Z (Z moved to end due to access)
2. Thread Safety Considerations
Like other Java collections, LinkedHashMap is not thread-safe. For concurrent access:
// Synchronized wrapper SequencedMap<String, Integer> safeMap = Collections.synchronizedSequencedMap( new LinkedHashMap<>() ); // Or use ConcurrentHashMap (but it doesn't implement SequencedMap) // For ordered concurrent maps, consider third-party libraries
3. Equality and Views
The reversed() view is backed by the original map:
LinkedHashMap<String, Integer> original = new LinkedHashMap<>();
original.put("A", 1);
original.put("B", 2);
SequencedMap<String, Integer> reversed = original.reversed();
System.out.println(reversed); // {B=2, A=1}
// Changes in original affect reversed view
original.put("C", 3);
System.out.println(reversed); // {C=3, B=2, A=1}
// reversed() is O(1) - it doesn't create a copy
Migration from Older Java Versions
If you're upgrading from pre-Java 21 code, the changes are largely additive:
// Pre-Java 21 LinkedHashMap<String, Integer> oldMap = new LinkedHashMap<>(); // ... populate map ... Iterator<Map.Entry<String, Integer>> it = oldMap.entrySet().iterator(); Map.Entry<String, Integer> first = it.next(); // Inefficient // Java 21+ SequencedMap<String, Integer> newMap = new LinkedHashMap<>(); // ... populate map ... Map.Entry<String, Integer> first = newMap.firstEntry(); // Efficient O(1)
When to Use LinkedHashMap as SequencedMap
Perfect use cases:
- LRU Caches: Natural fit with access-ordering
- Ordered configurations: Properties that need to maintain order
- Processing pipelines: Where element order matters
- Recent items tracking: Maintaining most-recently-used lists
- Session management: Tracking user actions in sequence
When to consider alternatives:
- Unordered data: Use
HashMapfor better performance - Concurrent access: Consider
ConcurrentHashMap(though it loses ordering) - Sorted data: Use
TreeMapfor natural ordering
Conclusion
The elevation of LinkedHashMap to a first-class SequencedMap citizen in Java 21 is more than just syntactic sugar—it's a fundamental improvement in API design. It eliminates the need for awkward, inefficient workarounds and provides a standardized contract for ordered map operations.
The SequencedMap interface finally gives LinkedHashMap the API it deserved all along, making ordered map operations intuitive, efficient, and self-documenting. Whether you're building caches, maintaining ordered configurations, or processing data sequences, the modern LinkedHashMap provides a clean, powerful solution that aligns with how developers naturally think about ordered collections.
This enhancement demonstrates Java's continued evolution toward more expressive, efficient, and intuitive APIs while maintaining backward compatibility—a win for both new and existing codebases.