Map Interface
java.util.Map is the interface for objects that associate keys with values. Each key appears at most once and maps to exactly one value. Maps answer the question "given this key, what is the value?", which is why they underpin caches, configuration, indexes, counters, and lookup tables throughout Java applications. HashMap, LinkedHashMap, TreeMap, EnumMap, ConcurrentHashMap, and the immutable maps from Map.of all implement it.
Map Is Not a Collection
Map sits in its own hierarchy, separate from Collection:
Map
├── HashMap
│ └── LinkedHashMap (SequencedMap, Java 21)
├── SortedMap
│ └── NavigableMap
│ └── TreeMap
├── EnumMap
├── ConcurrentMap
│ └── ConcurrentHashMap, ConcurrentSkipListMap
└── Hashtable (legacy)
A map holds pairs, so it does not have add(e) or a plain iterator. Instead, it provides three collection views that connect it to the rest of the framework:
| View | Type | Contents |
|---|---|---|
keySet() | Set<K> | The keys (a set, since keys are unique) |
values() | Collection<V> | The values (may contain duplicates) |
entrySet() | Set<Map.Entry<K, V>> | The key-value pairs |
The views are backed by the map: removing from a view removes from the map, and they reflect later changes to the map.
The Map Contract
| Rule | Meaning |
|---|---|
| Unique keys | put with an existing key replaces its value |
| One value per key | To associate several values with a key, make the value a collection |
| Key equality | Hash-based maps use equals/hashCode; sorted maps use compareTo/Comparator |
| Map equality | Two maps are equal if they contain the same key-value pairs, regardless of class or order |
Map<String, Integer> a = new HashMap<>(Map.of("a", 1, "b", 2));
Map<String, Integer> b = new TreeMap<>(Map.of("b", 2, "a", 1));
System.out.println(a.equals(b)); // true
Core Operations
| Method | Purpose |
|---|---|
put(k, v) | Insert or replace; returns the previous value or null |
get(k) | Value for k, or null |
getOrDefault(k, d) | Value for k, or d |
containsKey(k), containsValue(v) | Membership tests (the second scans all values) |
remove(k) | Remove the entry; returns the old value or null |
remove(k, v) | Remove only if k currently maps to v |
putIfAbsent(k, v) | Insert only if absent |
replace(k, v), replace(k, old, new) | Replace only if present (and, in the second form, only if equal to old) |
merge, compute, computeIfAbsent, computeIfPresent | Update values with a function |
putAll(m) | Copy all entries from another map |
forEach((k, v) -> ...), replaceAll((k, v) -> newV) | Act on or transform every entry |
size(), isEmpty(), clear() | Bookkeeping |
The HashMap lesson covers these methods in depth. They are defined by the Map interface, so they work identically on every implementation.
Map.Entry
Map.Entry<K, V> represents one key-value pair. You meet it when iterating entrySet():
Map<String, Integer> stock = new TreeMap<>(Map.of("pen", 120, "bag", 0, "ink", 35));
for (Map.Entry<String, Integer> e : stock.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
Entries can be created directly with Map.entry(k, v) (immutable), and sorted with ready-made comparators:
List<Map.Entry<String, Integer>> entries = new ArrayList<>(stock.entrySet());
entries.sort(Map.Entry.comparingByValue(Comparator.reverseOrder())); // highest stock first
Map.Entry.comparingByKey() and comparingByValue() accept an optional comparator for the key or value.
Programming to the Map Interface
Map<String, Integer> wordCounts = new HashMap<>();
public Map<String, List<Order>> groupByCustomer(List<Order> orders) { ... }
Declare variables, parameters, and return types as Map so the implementation can change. Use SortedMap or NavigableMap as the declared type when code depends on key order or methods like floorKey, and SequencedMap (Java 21) when it depends on a defined encounter order:
SequencedMap<String, Integer> steps = new LinkedHashMap<>();
steps.put("x", 1);
steps.put("y", 2);
steps.put("z", 3);
System.out.println(steps.firstEntry()); // x=1
System.out.println(steps.lastEntry()); // z=3
System.out.println(steps.reversed()); // {z=3, y=2, x=1}
SequencedMap also provides pollFirstEntry, pollLastEntry, putFirst, and putLast.
Choosing an Implementation
| Implementation | Key order | get / put | Notes |
|---|---|---|---|
HashMap | None | O(1) average | General default |
LinkedHashMap | Insertion order (or access order) | O(1) average | Predictable iteration; basis for LRU caches |
TreeMap | Sorted by key | O(log n) | Range and nearest-key queries |
EnumMap | Enum declaration order | O(1) | Enum keys only; array-backed, very compact |
Map.of / Map.copyOf | Unspecified | O(1) | Immutable; no null keys or values |
ConcurrentHashMap | None | O(1) average | Thread-safe without locking the whole map |
Hashtable | None | O(1) average | Legacy synchronized map; avoid in new code |
Immutable Maps
Map<String, Integer> limits = Map.of("free", 1, "pro", 5, "team", 25);
Map<String, String> codes = Map.ofEntries(
Map.entry("IN", "India"),
Map.entry("JP", "Japan"),
Map.entry("US", "United States")
);
Map<String, Integer> snapshot = Map.copyOf(liveMap);
These maps are unmodifiable, reject null, and have no defined iteration order. Map.of throws IllegalArgumentException for duplicate keys. Collections.unmodifiableMap(map) instead returns a read-only view that tracks the underlying map.
Common Map Patterns
Defaults overridden by specific values
Layer settings by copying defaults and then applying overrides with putAll:
Map<String, String> defaults = Map.of("theme", "light", "lang", "en", "pageSize", "20");
Map<String, String> user = Map.of("theme", "dark");
Map<String, String> effective = new TreeMap<>(defaults);
effective.putAll(user);
System.out.println(effective); // {lang=en, pageSize=20, theme=dark}
One key, many values (multimap)
Map<String, List<String>> coursesByStudent = new HashMap<>();
coursesByStudent.computeIfAbsent("ada", k -> new ArrayList<>()).add("Java");
coursesByStudent.computeIfAbsent("ada", k -> new ArrayList<>()).add("SQL");
// {ada=[Java, SQL]}
Counting
Map<Character, Integer> freq = new TreeMap<>();
for (char c : "banana".toCharArray()) {
freq.merge(c, 1, Integer::sum);
}
System.out.println(freq); // {a=3, b=1, n=2}
Inverting a map
Map<String, String> codeToCountry = Map.of("IN", "India", "JP", "Japan");
Map<String, String> countryToCode = new HashMap<>();
codeToCountry.forEach((code, country) -> countryToCode.put(country, code));
Inversion is safe only if the values are unique; otherwise later entries overwrite earlier ones.
Requirements on Keys
- Hash-based maps need keys with consistent
equalsandhashCode. - Sorted maps need keys that are
Comparableor aComparator; keys comparing as 0 are the same key. - Keys should be immutable. A key whose hash code or ordering changes after insertion becomes unreachable.
String, wrapper types, enums, records, and java.time types are all good keys. Values have no such requirements.
Practical Example: An LRU Cache
A least-recently-used (LRU) cache keeps a fixed number of entries and, when full, evicts the one that has gone unused the longest. LinkedHashMap supports this directly: it can keep entries in access order, and it calls a hook method after each insertion that can request removal of the oldest entry.
import java.util.*;
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LruCache(int capacity) {
super(16, 0.75f, true); // true = access order instead of insertion order
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // evict the least recently used entry
}
public static void main(String[] args) {
LruCache<String, String> pages = new LruCache<>(3);
pages.put("/home", "<home html>");
pages.put("/java", "<java html>");
pages.put("/docker", "<docker html>");
System.out.println("Cached: " + pages.keySet());
pages.get("/home"); // touch /home: now most recently used
pages.put("/django", "<django html>");
System.out.println("After adding /django: " + pages.keySet());
System.out.println("Hit /java? " + pages.containsKey("/java"));
System.out.println("Hit /docker? " + pages.containsKey("/docker"));
}
}
Output:
Cached: [/home, /java, /docker]
After adding /django: [/docker, /home, /django]
Hit /java? false
Hit /docker? true
The constructor's third argument, true, switches LinkedHashMap from insertion order to access order: every get or put of a key moves it to the end of the iteration order, so the first entry is always the least recently used. After each put, LinkedHashMap calls removeEldestEntry; returning true removes that first entry. When /django is added, /home has just been read, so /java is the oldest and is evicted. containsKey does not count as an access, so checking for a key does not change the order. Because LruCache is a Map, any code written against the Map interface can use it without knowing about eviction. (This class is not thread-safe; a cache shared between threads needs synchronization or a dedicated caching library.)