Java Map Interface
Java Map Interface
java.util.Map<K,V> is the interface that defines Java's key-value storage contract. Every HashMap, LinkedHashMap, and TreeMap you have ever used implements this interface — and understanding what it defines is what lets you switch between them confidently, write flexible API methods, and answer the Map-related interview questions that appear in almost every Java interview.
One thing is different about Map compared to every other collection type: it does not extend Collection. It stands in its own hierarchy, and every key must be unique. Values can repeat; keys cannot.
What Is the Map Interface?
Map<K,V> is an interface in java.util that models a mapping from unique keys to values. It is completely separate from java.util.Collection — Map is not a sub-interface of Collection, and Collection has no Map-related methods.
The diagram below shows the Map hierarchy alongside the Collection branch, making the separation clear.
JAVA COLLECTIONS FRAMEWORK — TWO INDEPENDENT BRANCHES
java.lang.Iterable
└── java.util.Collection ← single-element containers
├── List (ArrayList, LinkedList)
├── Set (HashSet, TreeSet)
└── Queue (ArrayDeque, PriorityQueue)
java.util.Map<K,V> ← key-value containers (SEPARATE)
├── HashMap (hash table — fastest, no order)
├── LinkedHashMap (hash table + insertion order)
├── Hashtable (legacy, synchronized)
└── SortedMap<K,V>
└── NavigableMap<K,V>
└── TreeMap (Red-Black tree — sorted keys)
Map is NOT a Collection. Passing a Map where Collection is expected
does not compile. Use map.keySet(), map.values(), or map.entrySet()
to get Collection views from a Map.
Every Method Defined by Map
CORE OPERATIONS: V put(K key, V value) ← add or replace; returns old value or null V get(Object key) ← retrieve value; returns null if absent V remove(Object key) ← remove entry; returns old value or null boolean containsKey(Object key) ← O(1) avg on HashMap boolean containsValue(Object value) ← O(n) always — values are not indexed int size() boolean isEmpty() void clear() BULK OPERATIONS: void putAll(Map<? extends K, ? extends V> m) Set<K> keySet() ← live Set view of all keys Collection<V> values() ← live Collection view of all values Set<Map.Entry<K,V>> entrySet() ← live Set view of all key-value pairs JAVA 8+ DEFAULT METHODS: V getOrDefault(Object key, V defaultValue) V putIfAbsent(K key, V value) boolean remove(Object key, Object value) ← conditional remove boolean replace(K key, V oldValue, V newValue) V replace(K key, V value) V computeIfAbsent(K key, Function<K,V> fn) V computeIfPresent(K key, BiFunction<K,V,V> fn) V compute(K key, BiFunction<K,V,V> fn) V merge(K key, V value, BiFunction<V,V,V> fn) void forEach(BiConsumer<K,V> action) void replaceAll(BiFunction<K,V,V> fn)
The three views returned by keySet(), values(), and entrySet() are live — they reflect every change made to the underlying map. Removing a key from keySet() removes the entry from the map itself.
When to Use Map
The three-question test for choosing a Map implementation:
Question 1: Do I need keys in sorted order or range navigation?
YES → TreeMap (NavigableMap: floorKey, ceilingKey, headMap, tailMap)
NO → go to question 2
Question 2: Do I need iteration in insertion order?
YES → LinkedHashMap (insertion order by default)
or LinkedHashMap(capacity, loadFactor, true) for LRU access-order
NO → go to question 3
Question 3: Do I just need the fastest put/get/remove?
YES → HashMap (default choice — O(1) average)
For thread safety:
Multiple threads reading and writing → ConcurrentHashMap
Rarely written, often read → Collections.synchronizedMap(new HashMap<>())
Internal Working of Map Implementations
HashMap — Hash Table
HashMap stores entries in an array of buckets. Each bucket holds a linked list of Node<K,V> entries. The bucket index for a key is computed from its hash code.
HashMap<String, Integer> map after storing four entries:
table[]: array of 16 buckets (default capacity)
index 0: null
index 1: Node("Mumbai", 18978000) → null
index 2: null
index 3: Node("Delhi", 16787941) → Node("Bengaluru", 8443675) → null
(two keys hashed to bucket 3 — collision chain)
index 4: Node("Chennai", 7088000) → null
...rest: null
put("Hyderabad", 6809970):
1. h = "Hyderabad".hashCode()
2. hash = h ^ (h >>> 16) — spreads high bits into low bits
3. index = (n - 1) & hash — n=16, so index is 0..15
4. Check table[index]:
a. Empty → create Node, store it
b. Key match (equals) → update value, return old value
c. Different key (collision) → append to chain
5. if (++size > threshold) resize — threshold = capacity * loadFactor (default 0.75)
Java 8 improvement: when a chain exceeds 8 entries (TREEIFY_THRESHOLD),
the linked list converts to a Red-Black tree — O(log n) instead of O(n)
for that bucket's worst case.
The hash spreading step — h ^ (h >>> 16) — is why HashMap performs well even with poor hash codes. By XOR-ing the high 16 bits of the hash into the low 16 bits, more information enters the bucket index calculation.
LinkedHashMap — Hash Table + Doubly Linked List
LinkedHashMap extends HashMap and maintains a separate doubly-linked list that threads through all entries in insertion order. Every put() appends to the tail of this list. Iteration traverses the linked list — predictable, consistent insertion order.
TreeMap — Red-Black Tree
TreeMap stores entries in a self-balancing binary search tree. Every key comparison uses the key's Comparable natural ordering or a Comparator provided at construction. The tree's in-order traversal always yields keys in sorted ascending order. Height is bounded at 2 × log₂(n+1), guaranteeing O(log n) for all operations regardless of insertion order.
Core Operations with Examples
put, get, and the Return Value
put(key, value) returns the previous value associated with the key, or null if no mapping existed. This return value is often ignored but is valuable when you need to detect whether an update happened or a fresh insert occurred.
1// File: MapPutGetDemo.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class MapPutGetDemo {
7
8 public static void main(String[] args) {
9
10 Map<String, Integer> cityPopulation = new HashMap<>();
11
12 // put — returns null on a fresh key (no previous value)
13 System.out.println("=== put() return values ===");
14 System.out.println("put(Mumbai, 18978000) : " + cityPopulation.put("Mumbai", 18978000));
15 System.out.println("put(Delhi, 16787941) : " + cityPopulation.put("Delhi", 16787941));
16 System.out.println("put(Chennai, 7088000) : " + cityPopulation.put("Chennai", 7088000));
17
18 // put on an existing key — returns the OLD value, replaces with new value
19 Integer oldValue = cityPopulation.put("Mumbai", 20000000); // updated figure
20 System.out.println("put(Mumbai, 20000000) : " + oldValue + " (was replaced)");
21 System.out.println("get(Mumbai) now : " + cityPopulation.get("Mumbai"));
22
23 System.out.println();
24
25 // get — returns null for absent keys (not an exception)
26 System.out.println("=== get() ===");
27 System.out.println("get(Delhi) : " + cityPopulation.get("Delhi"));
28 System.out.println("get(Hyderabad) : " + cityPopulation.get("Hyderabad")); // null
29
30 // getOrDefault — avoids null with a fallback value
31 System.out.println("getOrDefault(Hyderabad, 0): " +
32 cityPopulation.getOrDefault("Hyderabad", 0));
33
34 System.out.println();
35 System.out.println("=== size and containsKey ===");
36 System.out.println("size() : " + cityPopulation.size());
37 System.out.println("containsKey(Delhi) : " + cityPopulation.containsKey("Delhi"));
38 System.out.println("containsKey(Pune) : " + cityPopulation.containsKey("Pune"));
39 System.out.println("containsValue(7088000): " + cityPopulation.containsValue(7088000));
40 }
41}Output:
=== put() return values ===
put(Mumbai, 18978000) : null
put(Delhi, 16787941) : null
put(Chennai, 7088000) : null
put(Mumbai, 20000000) : 18978000 (was replaced)
get(Mumbai) now : 20000000
=== get() ===
get(Delhi) : 16787941
get(Hyderabad) : null
getOrDefault(Hyderabad, 0): 0
=== size and containsKey ===
size() : 3
containsKey(Delhi) : true
containsKey(Pune) : false
containsValue(7088000): true
remove and Conditional Operations
remove(key) removes the entry and returns the old value. Java 8 added putIfAbsent, computeIfAbsent, and merge — these solve common patterns that previously required verbose null checks.
1// File: MapRemoveAndJava8Demo.java
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.List;
6import java.util.Map;
7
8public class MapRemoveAndJava8Demo {
9
10 public static void main(String[] args) {
11
12 Map<String, Integer> stock = new HashMap<>();
13 stock.put("Laptop", 50); stock.put("Mouse", 200);
14 stock.put("Keyboard", 75); stock.put("Monitor", 30);
15
16 // remove — returns old value
17 System.out.println("=== remove ===");
18 System.out.println("remove(Monitor) : " + stock.remove("Monitor")); // 30
19 System.out.println("remove(Webcam) : " + stock.remove("Webcam")); // null — not present
20 System.out.println("After removes : " + stock);
21
22 System.out.println();
23
24 // putIfAbsent — inserts only if the key does not already exist
25 System.out.println("=== putIfAbsent ===");
26 stock.putIfAbsent("Webcam", 45); // Webcam absent — inserts
27 stock.putIfAbsent("Laptop", 999); // Laptop present — no change
28 System.out.println("After putIfAbsent: " + stock);
29
30 System.out.println();
31
32 // computeIfAbsent — builds the value lazily using a function
33 // Most commonly used to initialise a list value for a multi-value map
34 System.out.println("=== computeIfAbsent (multi-value map) ===");
35 Map<String, List<String>> ordersByCity = new HashMap<>();
36
37 String[][] orders = {
38 {"Mumbai", "ORD-101"}, {"Delhi", "ORD-102"},
39 {"Mumbai", "ORD-103"}, {"Delhi", "ORD-104"},
40 {"Bengaluru","ORD-105"}
41 };
42 for (String[] order : orders) {
43 ordersByCity.computeIfAbsent(order[0], k -> new ArrayList<>()).add(order[1]);
44 }
45 ordersByCity.forEach((city, orderList) ->
46 System.out.println(" " + city + " → " + orderList));
47
48 System.out.println();
49
50 // merge — accumulates a value; handles both first occurrence and updates
51 System.out.println("=== merge (frequency counter) ===");
52 Map<String, Integer> tagCount = new HashMap<>();
53 String[] tags = {"java","spring","java","hibernate","java","spring"};
54 for (String tag : tags) {
55 tagCount.merge(tag, 1, Integer::sum);
56 }
57 System.out.println("Tag counts: " + tagCount);
58 }
59}Output:
=== remove ===
remove(Monitor) : 30
remove(Webcam) : null
After removes : {Laptop=50, Mouse=200, Keyboard=75}
=== putIfAbsent ===
After putIfAbsent: {Laptop=50, Mouse=200, Keyboard=75, Webcam=45}
=== computeIfAbsent (multi-value map) ===
Mumbai → [ORD-101, ORD-103]
Delhi → [ORD-102, ORD-104]
Bengaluru → [ORD-105]
=== merge (frequency counter) ===
Tag counts: {java=3, spring=2, hibernate=1}
Iterating a Map — Three Patterns
Iterating a Map correctly is a common interview topic. The entrySet() pattern is almost always preferred over keySet() when both key and value are needed.
1// File: MapIterationDemo.java
2
3import java.util.HashMap;
4import java.util.LinkedHashMap;
5import java.util.Map;
6import java.util.TreeMap;
7
8public class MapIterationDemo {
9
10 public static void main(String[] args) {
11
12 Map<String, Double> productPrices = new LinkedHashMap<>(); // insertion order
13 productPrices.put("Laptop", 45999.0);
14 productPrices.put("Mouse", 599.0);
15 productPrices.put("Keyboard", 1299.0);
16 productPrices.put("Monitor", 12999.0);
17
18 // Pattern 1 — entrySet() — best when both key and value are needed
19 System.out.println("=== entrySet() — key + value ===");
20 for (Map.Entry<String, Double> entry : productPrices.entrySet()) {
21 System.out.printf(" %-12s Rs. %.2f%n",
22 entry.getKey(), entry.getValue());
23 }
24
25 // Pattern 2 — keySet() — only when the key alone is needed
26 System.out.println("\n=== keySet() — keys only ===");
27 for (String key : productPrices.keySet()) {
28 System.out.println(" " + key);
29 }
30
31 // Pattern 3 — values() — only when the value alone is needed
32 System.out.println("\n=== values() — values only ===");
33 double total = 0;
34 for (double price : productPrices.values()) {
35 total += price;
36 }
37 System.out.printf(" Total value: Rs. %.2f%n", total);
38
39 // Pattern 4 — forEach (Java 8 BiConsumer) — cleanest for simple operations
40 System.out.println("\n=== forEach lambda ===");
41 productPrices.forEach((product, price) ->
42 System.out.printf(" %-12s Rs. %.2f%n", product, price));
43
44 // TreeMap — iteration always in sorted key order
45 System.out.println("\n=== TreeMap — sorted key order ===");
46 Map<String, Double> sorted = new TreeMap<>(productPrices);
47 sorted.forEach((product, price) ->
48 System.out.printf(" %-12s Rs. %.2f%n", product, price));
49 }
50}Output:
=== entrySet() — key + value ===
Laptop Rs. 45999.00
Mouse Rs. 599.00
Keyboard Rs. 1299.00
Monitor Rs. 12999.00
=== keySet() — keys only ===
Laptop
Mouse
Keyboard
Monitor
=== values() — values only ===
Total value: Rs. 60896.00
=== forEach lambda ===
Laptop Rs. 45999.00
Mouse Rs. 599.00
Keyboard Rs. 1299.00
Monitor Rs. 12999.00
=== TreeMap — sorted key order ===
Keyboard Rs. 1299.00
Laptop Rs. 45999.00
Monitor Rs. 12999.00
Mouse Rs. 599.00
HashMap vs LinkedHashMap vs TreeMap Side by Side
1// File: MapImplementationsDemo.java
2
3import java.util.HashMap;
4import java.util.LinkedHashMap;
5import java.util.Map;
6import java.util.TreeMap;
7
8public class MapImplementationsDemo {
9
10 public static void main(String[] args) {
11
12 String[] insertOrder = {"Zomato", "Swiggy", "Flipkart", "Meesho", "Razorpay"};
13 int[] foundedYear = {2008, 2014, 2007, 2015, 2014};
14
15 // HashMap — fastest, key order unpredictable
16 Map<String, Integer> hashMap = new HashMap<>();
17 for (int i = 0; i < insertOrder.length; i++) {
18 hashMap.put(insertOrder[i], foundedYear[i]);
19 }
20 System.out.println("HashMap (no order guarantee):");
21 hashMap.forEach((k, v) -> System.out.println(" " + k + " → " + v));
22
23 // LinkedHashMap — same speed, preserves insertion order
24 Map<String, Integer> linkedMap = new LinkedHashMap<>();
25 for (int i = 0; i < insertOrder.length; i++) {
26 linkedMap.put(insertOrder[i], foundedYear[i]);
27 }
28 System.out.println("\nLinkedHashMap (insertion order):");
29 linkedMap.forEach((k, v) -> System.out.println(" " + k + " → " + v));
30
31 // TreeMap — keys always in sorted alphabetical order
32 Map<String, Integer> treeMap = new TreeMap<>();
33 for (int i = 0; i < insertOrder.length; i++) {
34 treeMap.put(insertOrder[i], foundedYear[i]);
35 }
36 System.out.println("\nTreeMap (sorted keys):");
37 treeMap.forEach((k, v) -> System.out.println(" " + k + " → " + v));
38
39 // TreeMap NavigableMap methods — only available on sorted maps
40 System.out.println("\nNavigableMap operations on TreeMap:");
41 System.out.println(" firstKey() : " + ((TreeMap<String,Integer>)treeMap).firstKey());
42 System.out.println(" lastKey() : " + ((TreeMap<String,Integer>)treeMap).lastKey());
43 System.out.println(" floorKey(Meesho) : " +
44 ((TreeMap<String,Integer>)treeMap).floorKey("Meesho")); // largest key <= "Meesho"
45 System.out.println(" headMap(Meesho) : " +
46 ((TreeMap<String,Integer>)treeMap).headMap("Meesho")); // keys < "Meesho"
47 }
48}Output:
HashMap (no order guarantee):
Razorpay → 2014
Meesho → 2015
Swiggy → 2014
Zomato → 2008
Flipkart → 2007
LinkedHashMap (insertion order):
Zomato → 2008
Swiggy → 2014
Flipkart → 2007
Meesho → 2015
Razorpay → 2014
TreeMap (sorted keys):
Flipkart → 2007
Meesho → 2015
Razorpay → 2014
Swiggy → 2014
Zomato → 2008
NavigableMap operations on TreeMap:
firstKey() : Flipkart
lastKey() : Zomato
floorKey(Meesho) : Meesho
headMap(Meesho) : {Flipkart=2007}
Real-World Example — PhonePe Merchant Transaction Ledger
A payments platform like PhonePe tracks transactions per merchant and needs three things simultaneously: O(1) lookup by merchant ID for real-time API responses, insertion-order iteration for audit logs, and a sorted view for the dashboard report. The Map interface makes all three possible by switching the implementation without changing the business logic.
1// File: Transaction.java
2
3public class Transaction {
4
5 private final String transactionId;
6 private final double amount;
7 private final String status;
8 private final String timestamp;
9
10 public Transaction(String transactionId, double amount,
11 String status, String timestamp) {
12 this.transactionId = transactionId;
13 this.amount = amount;
14 this.status = status;
15 this.timestamp = timestamp;
16 }
17
18 public double getAmount() { return amount; }
19 public String getStatus() { return status; }
20 public String getTransactionId() { return transactionId; }
21
22 @Override
23 public String toString() {
24 return String.format("[%s] Rs.%.2f %-9s %s",
25 transactionId, amount, status, timestamp);
26 }
27}1// File: MerchantLedger.java
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.LinkedHashMap;
6import java.util.List;
7import java.util.Map;
8import java.util.TreeMap;
9
10public class MerchantLedger {
11
12 // Fast lookup by merchant ID — O(1) per query
13 private final Map<String, List<Transaction>> ledger = new HashMap<>();
14
15 // Audit trail — maintains event arrival order
16 private final Map<String, String> auditLog = new LinkedHashMap<>();
17
18 public void recordTransaction(String merchantId, Transaction txn) {
19 // computeIfAbsent: creates the list on first transaction for this merchant
20 ledger.computeIfAbsent(merchantId, id -> new ArrayList<>()).add(txn);
21 auditLog.put(txn.getTransactionId(), merchantId + " | " + txn);
22 }
23
24 // O(1) — direct HashMap lookup for real-time API response
25 public List<Transaction> getMerchantTransactions(String merchantId) {
26 return ledger.getOrDefault(merchantId, List.of());
27 }
28
29 public double getMerchantRevenue(String merchantId) {
30 return getMerchantTransactions(merchantId).stream()
31 .filter(t -> "SUCCESS".equals(t.getStatus()))
32 .mapToDouble(Transaction::getAmount)
33 .sum();
34 }
35
36 // Returns revenue by merchant in sorted order — TreeMap for the report
37 public Map<String, Double> revenueReport() {
38 Map<String, Double> report = new TreeMap<>(); // sorted by merchant ID
39 ledger.forEach((merchantId, transactions) -> {
40 double revenue = transactions.stream()
41 .filter(t -> "SUCCESS".equals(t.getStatus()))
42 .mapToDouble(Transaction::getAmount)
43 .sum();
44 report.put(merchantId, revenue);
45 });
46 return report;
47 }
48
49 public static void main(String[] args) {
50
51 MerchantLedger ledger = new MerchantLedger();
52
53 ledger.recordTransaction("M-SWIGGY",
54 new Transaction("TXN001", 1299.00, "SUCCESS", "10:01:05"));
55 ledger.recordTransaction("M-ZOMATO",
56 new Transaction("TXN002", 499.00, "SUCCESS", "10:01:18"));
57 ledger.recordTransaction("M-SWIGGY",
58 new Transaction("TXN003", 849.00, "FAILED", "10:02:30"));
59 ledger.recordTransaction("M-FLIPKART",
60 new Transaction("TXN004", 5499.00, "SUCCESS", "10:03:12"));
61 ledger.recordTransaction("M-ZOMATO",
62 new Transaction("TXN005", 749.00, "SUCCESS", "10:03:55"));
63 ledger.recordTransaction("M-SWIGGY",
64 new Transaction("TXN006", 399.00, "SUCCESS", "10:04:20"));
65
66 System.out.println("╔══════════════════════════════════════════════╗");
67 System.out.println("║ PHONEPE MERCHANT LEDGER REPORT ║");
68 System.out.println("╚══════════════════════════════════════════════╝");
69
70 System.out.println("\n=== O(1) lookup: Swiggy transactions ===");
71 ledger.getMerchantTransactions("M-SWIGGY")
72 .forEach(t -> System.out.println(" " + t));
73
74 System.out.println("\n=== Revenue per merchant (sorted report) ===");
75 ledger.revenueReport().forEach((merchantId, revenue) ->
76 System.out.printf(" %-14s Rs. %.2f%n", merchantId, revenue));
77
78 System.out.println("\n=== Audit log (insertion order via LinkedHashMap) ===");
79 ledger.auditLog.forEach((txnId, detail) ->
80 System.out.println(" " + txnId + " → " + detail));
81
82 System.out.println("\n=== Swiggy revenue (SUCCESS only) ===");
83 System.out.printf(" Rs. %.2f%n",
84 ledger.getMerchantRevenue("M-SWIGGY"));
85 }
86}Output:
╔══════════════════════════════════════════════╗
║ PHONEPE MERCHANT LEDGER REPORT ║
╚══════════════════════════════════════════════╝
=== O(1) lookup: Swiggy transactions ===
[TXN001] Rs.1299.00 SUCCESS 10:01:05
[TXN003] Rs.849.00 FAILED 10:02:30
[TXN006] Rs.399.00 SUCCESS 10:04:20
=== Revenue per merchant (sorted report) ===
M-FLIPKART Rs. 5499.00
M-SWIGGY Rs. 1698.00
M-ZOMATO Rs. 1248.00
=== Audit log (insertion order via LinkedHashMap) ===
TXN001 → M-SWIGGY | [TXN001] Rs.1299.00 SUCCESS 10:01:05
TXN002 → M-ZOMATO | [TXN002] Rs.499.00 SUCCESS 10:01:18
TXN003 → M-SWIGGY | [TXN003] Rs.849.00 FAILED 10:02:30
TXN004 → M-FLIPKART | [TXN004] Rs.5499.00 SUCCESS 10:03:12
TXN005 → M-ZOMATO | [TXN005] Rs.749.00 SUCCESS 10:03:55
TXN006 → M-SWIGGY | [TXN006] Rs.399.00 SUCCESS 10:04:20
=== Swiggy revenue (SUCCESS only) ===
Rs. 1698.00
Three Map implementations, each solving a distinct requirement — HashMap for O(1) lookup, LinkedHashMap for ordered audit trail, TreeMap for the sorted revenue report — all written against the same Map<K,V> interface.
Performance Considerations
| Operation | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| put(key, value) | O(1) avg | O(1) avg | O(log n) |
| get(key) | O(1) avg | O(1) avg | O(log n) |
| remove(key) | O(1) avg | O(1) avg | O(log n) |
| containsKey(key) | O(1) avg | O(1) avg | O(log n) |
| containsValue(v) | O(n) | O(n) | O(n) |
| Iteration order | Undefined | Insertion | Sorted |
| Memory overhead | Moderate | Higher (+linked list) | Higher (+tree pointers) |
Thread safety: None of the standard Map implementations are thread-safe. For concurrent access, use ConcurrentHashMap from java.util.concurrent — it uses lock striping (Java 7) or CAS operations (Java 8+) instead of locking the entire map. Collections.synchronizedMap(new HashMap<>()) is a simpler alternative but locks on every single operation.
HashMap resizing: Default capacity is 16, load factor is 0.75. Rehashing occurs when size > capacity × 0.75. Each rehash doubles the bucket count and copies all entries. Pre-size with new HashMap<>((int)(expectedSize / 0.75) + 1) to prevent multiple resizes during bulk loading.
Best Practices
Always declare variables and parameters as Map<K,V>, not HashMap<K,V>. Writing Map<String, Integer> scores = new HashMap<>() means you can swap the implementation to TreeMap or LinkedHashMap by changing one line. Declaring it as HashMap<String, Integer> locks callers to one implementation with no benefit. During code reviews at teams following clean code principles, seeing the concrete type on the left is flagged immediately.
Use getOrDefault() to avoid null checks on absent keys. map.getOrDefault(key, defaultValue) is cleaner and more expressive than map.get(key) != null ? map.get(key) : defaultValue. It also avoids the double-lookup cost of calling get() twice. The same logic applies to computeIfAbsent() for the lazy-initialisation pattern.
Use merge() for accumulating values. map.merge(key, 1, Integer::sum) handles both the first occurrence (inserts 1) and all subsequent occurrences (adds 1 to existing) in one atomic call. The pre-Java-8 pattern — check containsKey, then put — requires two method calls and is not atomic in concurrent code.
Iterate with entrySet() when both key and value are needed. Iterating with keySet() then calling map.get(key) inside the loop performs two hash lookups per entry — one for iteration, one for the get. entrySet() gives both in a single traversal step. During code reviews, seniors flag the keySet() + get() pattern in hot paths.
Common Mistakes
Mistake 1 — Using a Mutable Object as a Map Key
1// WRONG — if the key's hashCode changes after insertion, the entry becomes unreachable
2List<String> keyList = new ArrayList<>(List.of("admin", "user"));
3Map<List<String>, String> roleMap = new HashMap<>();
4roleMap.put(keyList, "ROLE_ADMIN");
5
6keyList.add("guest"); // mutates the key — hashCode changes!
7System.out.println(roleMap.get(keyList)); // null — entry is lost in wrong bucket
8
9// CORRECT — always use immutable keys
10Map<String, String> safeMap = new HashMap<>();
11safeMap.put("admin,user", "ROLE_ADMIN");
12// Or use List.of() which is immutableMistake 2 — Iterating keySet() When entrySet() Is Needed
1Map<String, Integer> stockMap = new HashMap<>();
2// Populate...
3
4// WRONG — two lookups per entry; the get() call is wasteful
5for (String sku : stockMap.keySet()) {
6 int quantity = stockMap.get(sku); // second hash computation + bucket search
7 System.out.println(sku + " = " + quantity);
8}
9
10// CORRECT — one lookup per entry
11for (Map.Entry<String, Integer> entry : stockMap.entrySet()) {
12 System.out.println(entry.getKey() + " = " + entry.getValue());
13}Mistake 3 — Confusing get() Returning null With "Key Not Present"
1Map<String, String> config = new HashMap<>();
2config.put("debugMode", null); // intentionally mapped to null
3
4String value = config.get("debugMode");
5// value is null — but the key IS present!
6
7// WRONG — cannot distinguish absent key from null-mapped key using get() alone
8if (config.get("debugMode") == null) {
9 System.out.println("Key not configured!"); // fires incorrectly
10}
11
12// CORRECT — use containsKey() to distinguish the two cases
13if (config.containsKey("debugMode")) {
14 System.out.println("Key present, value is: " + config.get("debugMode")); // "null"
15} else {
16 System.out.println("Key not configured!");
17}Mistake 4 — Structurally Modifying a Map During Iteration
1Map<String, Integer> activeUsers = new HashMap<>();
2// Populate...
3
4// WRONG — throws ConcurrentModificationException at runtime
5for (Map.Entry<String, Integer> entry : activeUsers.entrySet()) {
6 if (entry.getValue() == 0) {
7 activeUsers.remove(entry.getKey()); // modifies the map while iterating
8 }
9}
10
11// CORRECT — use entrySet().removeIf() or collect keys first
12activeUsers.entrySet().removeIf(entry -> entry.getValue() == 0);Interview Questions
Q1. What is the Map interface in Java and how is it different from Collection?
java.util.Map<K,V> is an interface in java.util that models key-value associations — each unique key maps to exactly one value. Unlike List, Set, and Queue, Map does not extend java.util.Collection. It has its own hierarchy. You cannot pass a Map where a Collection is expected; you must use one of the collection views: keySet(), values(), or entrySet(). The practical reason for this separation is that Map has a fundamentally different structure — every element is a pair, not a single item — so the Collection.add(element) contract does not apply cleanly.
Q2. What is the difference between HashMap, LinkedHashMap, and TreeMap?
All three implement Map<K,V> but differ in ordering and performance. HashMap uses a hash table — O(1) average for all key-based operations, no guaranteed iteration order. LinkedHashMap extends HashMap and maintains a doubly-linked list threading all entries — O(1) operations with iteration in insertion order (or access order if constructed with the access-order flag). TreeMap uses a Red-Black tree — O(log n) for all operations, but keys are always iterated in sorted natural order. Choose HashMap for speed, LinkedHashMap for reproducible order, TreeMap for sorted keys or range operations.
Q3. What happens when you call put() with a key that already exists?
The new value replaces the old one and put() returns the old value. The key itself is not updated — the existing key object remains in the map. This means if you later change the object you used as a key (if it is mutable), the hashCode changes and the entry becomes unreachable in the map — get() returns null even though the entry is still physically present in some bucket. This is why Map keys must always be immutable or at least never mutated after insertion. String, Integer, and records are safe choices.
Q4. What is the time complexity of containsValue() on a HashMap?
O(n) — always. containsKey() is O(1) because the hash table can jump directly to the right bucket. containsValue() has no such shortcut — values are not indexed. It must scan every bucket in the table and every node in each chain, checking each value for equality. For large maps where value lookup is frequent, the correct pattern is to maintain an inverse map (Map<V, K>) or switch to a bidirectional map structure. Calling containsValue() in a loop is a common performance issue that appears in fresher code during pull request reviews.
Q5. What is the purpose of entrySet() and why is it preferred over keySet() for iteration?
entrySet() returns a Set<Map.Entry<K,V>> where each entry holds both the key and value. Iterating it gives access to both without any additional lookup. keySet() returns only the keys — retrieving the value for each key requires a second map.get(key) call, which computes the hash again, finds the bucket again, and walks the chain again. For large maps with many entries, this doubles the work per iteration. entrySet() is the standard pattern that interviewers expect to see and that code review guidelines enforce.
Q6. What is the role of equals() and hashCode() in a HashMap?
They are the two-step key lookup mechanism. hashCode() determines which bucket an entry belongs to — two equal keys must produce the same hash code and land in the same bucket. equals() identifies the exact entry within the bucket chain — when a bucket has multiple entries (collision), equals() is called on each one until a match is found. Violating either contract produces incorrect behaviour: two equal keys with different hash codes are stored as separate entries (a duplicate key bug), and two unequal objects with the same hash code produce collisions but still work correctly because equals() distinguishes them.
FAQs
Does Map allow null keys and null values?
It depends on the implementation. HashMap allows one null key and any number of null values. LinkedHashMap follows the same rules. TreeMap throws NullPointerException on a null key because it needs to call compareTo() on the key. Hashtable rejects both null keys and null values. ConcurrentHashMap rejects both. For new code, avoid null keys and null values in maps — the ambiguity between "key not present" and "key present with null value" causes real bugs.
Can you have duplicate values in a Map?
Yes. Map enforces uniqueness only on keys. Multiple keys can map to the same value — for example, multiple employees can have the same department code. map.containsValue(v) checks whether any key maps to that value and is an O(n) scan. If you need to look up keys by value efficiently, maintain a second inverse Map<V, K> alongside the original.
What is the difference between remove(key) and remove(key, value)?
remove(key) removes the entry for the key regardless of its current value and returns the old value. remove(key, value) is a conditional remove — it removes the entry only if the key is currently mapped to that exact value, returning true if the removal happened. The two-argument form was added in Java 8 and is useful for atomic compare-and-remove operations, particularly in concurrent code with ConcurrentHashMap where the operation is genuinely atomic.
What is an LRU cache and how do you implement one with a Map?
An LRU (Least Recently Used) cache evicts the entry that was accessed least recently when capacity is exceeded. Implement it with LinkedHashMap in access-order mode: new LinkedHashMap<>(capacity, 0.75f, true) where true means entries move to the tail on every get() and put(). Override removeEldestEntry() to return true when size() > maxCapacity. The head entry is always the least recently used. This is O(1) for all operations.
What is the difference between Map.of() and new HashMap()?
Map.of("key", value, ...) creates a fully immutable Map — put(), remove(), and clear() throw UnsupportedOperationException. It accepts up to 10 key-value pairs directly; for more, use Map.ofEntries(Map.entry(k,v), ...). Map.of() also rejects null keys and values. new HashMap<>() creates a mutable map with no restrictions. Use Map.of() for constants and configuration; use new HashMap<>() for maps that change at runtime.
How do you sort a Map by value in Java?
Map does not support sorting by value natively — its sort order, if any, is by key. To sort by value, stream the entry set and sort: map.entrySet().stream().sorted(Map.Entry.comparingByValue()).collect(Collectors.toMap(..., LinkedHashMap::new)). The LinkedHashMap collector preserves the sorted order. For descending order, use Map.Entry.comparingByValue(Comparator.reverseOrder()). This pattern appears frequently in reporting and leaderboard features.
Summary
java.util.Map<K,V> is Java's key-value interface — separate from Collection, with unique keys and any values. HashMap is the default choice at O(1) average for all key operations. LinkedHashMap adds insertion-order iteration at minimal cost. TreeMap provides sorted key iteration and range navigation at O(log n) per operation.
The Map hierarchy gives you the flexibility to write a method against Map<K,V> and have it work with HashMap, TreeMap, and LinkedHashMap interchangeably. The Java 8 methods — getOrDefault, putIfAbsent, computeIfAbsent, merge, forEach — replace the verbose null-check patterns that dominated pre-Java-8 Map code.
For interviews: know why Map does not extend Collection, know the time complexity difference between containsKey() and containsValue(), explain the equals()/hashCode() contract for HashMap keys, and describe why mutable objects make dangerous keys. These questions appear consistently from fresher campus placements to senior engineering rounds at product companies.
What to Read Next
| Topic | Link |
|---|---|
| How HashMap implements the Map interface with hash tables and collision chains | Java HashMap → |
| How TreeMap implements SortedMap with a Red-Black tree for guaranteed O(log n) | Java TreeMap → |
| How the Collections Framework organises all Map and Collection types together | Java Collections Framework → |
| How Generics make Map type-safe and eliminate casting for keys and values | Java Generics → |
| How Java Streams process Map entries with filter, map, collect, and groupingBy | Java Streams API → |