Sixteen pigeonholes in the staff room
Arjun Sir's staff room has a wall of 16 pigeonholes for letters. A clerk doesn't search all 16: she takes the teacher's name, does a quick calculation that always gives the same answer for the same name, and drops the letter straight into that hole. When two teachers' names give the same hole, both letters sit there, and she checks the names when someone collects.
Sir, that's exactly what the canteen app needs. Prices by dish name, instantly.
And it's exactly what a HashMap does. The “quick calculation” is the key's hashCode(). The pigeonholes are an array of buckets. When two keys share a bucket, the entries are chained and equals() picks the right one.
What if the canteen grows to 500 dishes? Sixteen holes would be stuffed.
Then the map does what the clerk would: when it's 75% full, it builds a wall with twice as many holes and re-sorts the letters. Let's watch every step happen in a real JVM.
What is a HashMap?
java.util.HashMap<K,V> is a hash-table based implementation of the Map interface that stores key–value pairs with unique keys. It permits one null key and any number of null values, makes no guarantee about iteration order, and is not synchronised. Internally it keeps an array of buckets (table); a key's bucket index is computed from its hashCode() (spread by hash()) as (n − 1) & hash. Entries in the same bucket form a linked list, converted to a balanced tree when a bucket grows large. When the number of entries exceeds capacity × load factor (default 16 × 0.75 = 12), the table doubles and entries are redistributed. Average time for get and put is O(1).
A dictionary: give it a key, get the value. Speed comes from computing where the key lives instead of searching for it.
Words you'll meet in this lesson
Node{hash, key, value, next}.table array; holds zero, one or a chain of nodes.| Default capacity / load factor | 16 buckets, 0.75 → first resize when the 13th entry is added |
|---|---|
| Bucket index | (n − 1) & hash, where hash = h ^ (h >>> 16) and h = key.hashCode() |
| Collisions | Chained in a linked list (new nodes at the tail); a tree when a chain reaches 8 and capacity ≥ 64 |
| Null | One null key (always bucket 0), many null values. Hashtable allows neither |
| Cost | get/put/remove O(1) average; worst case O(log n) with treeified buckets |
Exam tip: the steps of put(k, v): (1) hash(k), (2) index = (n − 1) & hash, (3) empty bucket → new node; otherwise walk the chain comparing hash and equals: same key → replace the value (and return the old one), else append a node, (4) if ++size > threshold → resize to double capacity. Learn this sequence word for word.
Using a HashMap well
Put and update
put, putIfAbsent, merge, compute.
Look up
get, getOrDefault, containsKey, containsValue.
Iterate
entrySet(), keySet(), values(), forEach.
Ordered cousins
LinkedHashMap (insertion order), TreeMap (sorted keys).
Everyday operations
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> price = new HashMap<>();
System.out.println(price.put("chai", 15)); // null: no old value
System.out.println(price.put("chai", 20)); // 15: old value replaced
price.put("dosa", 60);
price.putIfAbsent("dosa", 99); // ignored: key exists
System.out.println(price.get("chai") + " " + price.get("tea"));
System.out.println(price.getOrDefault("tea", 0) + " " + price.containsKey("dosa"));
Map<String, Integer> sold = new HashMap<>();
for (String d : new String[]{"chai", "dosa", "chai"}) {
sold.merge(d, 1, Integer::sum); // count: start at 1 or add 1
}
for (Map.Entry<String, Integer> e : sold.entrySet()) {
System.out.println(e.getKey() + " sold " + e.getValue());
}
}
}merge and Integer::sum use a method reference (Lesson 14.1); read it as “if the key is new, store 1, otherwise add 1 to the old value”.
Collisions are normal
public class Main {
public static void main(String[] args) {
// different keys, same hash code: a true collision
System.out.println("Aa".hashCode() + " " + "BB".hashCode());
// different hash codes, same bucket in a 16-bucket table
System.out.println(index("idli", 16) + " " + index("samosa", 16));
System.out.println(index("idli", 32) + " " + index("samosa", 32)); // after doubling
}
static int index(String key, int buckets) {
int h = key.hashCode();
int hash = h ^ (h >>> 16); // HashMap.hash(): mix high bits into low bits
return (buckets - 1) & hash; // same as hash % buckets for powers of two
}
}With 16 buckets, “idli” and “samosa” share bucket 9. When the table grows to 32, one more bit of the hash is used and they separate: idli stays at 9, samosa moves to 9 + 16 = 25.
The mutable-key bug
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> seat = new ArrayList<>(List.of(4, 32)); // coach 4, seat 32
Map<List<Integer>, String> booked = new HashMap<>();
booked.put(seat, "Riya");
seat.set(1, 33); // key changed after being stored!
System.out.println(booked.get(seat) + " " + booked.get(List.of(4, 32)) + " " + booked.size());
}
}Keys must not change while in a map. The entry sits in the bucket chosen by the old hash. After the change, the new hash points to another bucket, and the old key no longer equals anything you can build. The entry is stuck: size 1, but unreachable. Use immutable keys: String, Integer, enums, records.
Rule of thumb: HashMap for lookups; LinkedHashMap when output should follow insertion order; TreeMap when keys must be sorted or you need floorKey/ceilingKey; ConcurrentHashMap when several threads write. If you know the size, pre-size it: new HashMap<>(expected * 4 / 3 + 1), or HashMap.newHashMap(expected) on Java 19+.
Inside put("samosa", 20)
Compute the hash
Only the low bits pick the bucket, soint h = key.hashCode(); // "samosa" → -909675842 int hash = h ^ (h >>> 16); // spread the high bits downhash()mixes the top 16 bits into the bottom 16. That way keys that differ only in high bits don't all collide.Find the bucket
index = (n − 1) & hash. With n = 16,n − 1is binary 1111, so this keeps the last 4 bits: a number from 0 to 15. Faster than%, and the reason capacity is always a power of two.Bucket occupied? Walk the chain
Bucket 9 already holds “idli”. For each node, the map compares the stored hash first (cheap) and thenequals. “idli” ≠ “samosa”, so it keeps walking. If an equal key were found, its value would be replaced and the old value returned.Append a new node
ANode{hash, "samosa", 20, next=null}is linked after “idli”. If this chain reached 8 nodes, the bucket would be converted to a red-black tree (or the table resized, if it has fewer than 64 buckets).Check the threshold
++size > threshold? With capacity 16 the threshold is 12, so the 13th entry triggersresize(): a new table of 32, and every node moves either to the same index or to index + 16, depending on one extra hash bit. No full rehash of keys is needed.
put, collide, get, resize: both views
Left: map calls in your code. Right: the bucket table, a chain forming in bucket 9, and what happens when the table doubles.
puta new key returnsnull: there was no old value.- Another new key.
- And another. Nothing on screen hints that anything special happened.
puton an existing key replaces the value and returns the old one, 15. Size stays 3.get("samosa")returns 20.- Many dishes later, the 13th entry is added. Your code doesn't change; the map reorganises itself.
hash("chai")mixes the hash code's bits;(16 − 1) & hash= 3. Bucket 3 is empty, so a new node goes there.- "idli" lands in bucket 9.
- "samosa" also lands in bucket 9: a collision. The map compares with idli (hash, then
equals): different key, so the new node is linked after idli. - "chai" again: bucket 3, the existing node's key
equals"chai", so only the value changes (15 → 20). The old value is returned. get("samosa")computes the same index 9, checks idli (not equal), moves to the next node, finds samosa and returns 20.- When the 13th entry pushes size past the threshold 12, the table doubles to 32. Each node moves to the same index or index + 16, decided by one extra hash bit: idli stays at 9, samosa goes to 25. The chain is gone.
HashMap in real systems
Hash maps are the workhorse of backend code: caches, indexes, counters and lookups.
Watch it with an example: an IFSC lookup at a bank
A payments service maps IFSC codes to branch details. Watch one lookup: no scanning of thousands of branches, just a computed jump and one or two equals checks.
- A transfer request carries the IFSC code
SBIN0001234. - The code's
hashCode()is spread and masked into a bucket index. The other 4,095 buckets are never touched. - That bucket happens to hold two entries. One
equalscheck fails, the next succeeds. - The branch details come back in a few steps, whether the map holds 100 or 1,50,000 branches. (Simplified: real systems also validate the code format and cache results.)
1. Sessions and caches
Web servers map session IDs to logged-in users; services cache exchange rates, product details and config by key to avoid slow database calls.
2. Counting and grouping
Orders per city, errors per type, votes per candidate: merge(key, 1, Integer::sum) or computeIfAbsent(key, k -> new ArrayList<>()).add(item) for grouping.
3. Joining data in memory
To combine orders with customers, load customers into a map by ID once, then look each order's customer up in O(1), instead of a nested loop.
4. Hash flooding and bad hash codes
Attackers once sent web servers thousands of parameters with colliding hash codes to slow them down. Java 8's treeified buckets cap the damage at O(log n). A home-made hashCode() that returns a constant causes the same slowdown by accident.
Examples are simplified. Shared caches across servers use stores like Redis; inside one JVM with many threads, ConcurrentHashMap.
Canteen billing with a price map and a sales counter
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> price = new HashMap<>();
price.put("chai", 15);
price.put("idli", 30);
price.put("samosa", 20);
price.put("dosa", 60);
String[] orders = {"chai", "idli", "chai", "pav", "samosa", "chai"};
Map<String, Integer> sold = new TreeMap<>(); // sorted report
int bill = 0;
List<String> unknown = new ArrayList<>();
for (String item : orders) {
Integer p = price.get(item);
if (p == null) {
unknown.add(item);
continue;
}
bill += p;
sold.merge(item, 1, Integer::sum);
}
System.out.println("Sold: " + sold);
System.out.println("Bill: ₹" + bill + ", not on menu: " + unknown);
for (Map.Entry<String, Integer> e : sold.entrySet()) {
System.out.println(e.getKey() + " → ₹" + e.getValue() * price.get(e.getKey()));
}
}
}price.get(item)O(1) lookup; returnsnullfor an unknown dish, so the result is kept asInteger.sold.merge(item, 1, Integer::sum)First time: puts 1. Later: adds 1.new TreeMap<>()The report prints dishes alphabetically.entrySet()The efficient way to walk keys and values together.
A real collision in bucket 9
This trace is recorded from the real JVM. The heap line shows the map's bucket table: [9] "idli"=30 → "samosa"=20 means two entries chained in bucket 9. Watch the update of an existing key and a lookup that walks the chain.
Worked examples: real problems, solved and traced in memory
Three classic HashMap tasks, traced on the real JVM. The first one shows a resize happening.
Watch the table double at the 13th entry
Problem statement
Add 13 menu items to a HashMap and observe, on the heap, the table growing from 16 to 32 buckets and the idli/samosa chain splitting.
13 dishes
13 dishes
Approach
- Put the dishes in a loop.
- Watch the heap line: “16 buckets” until size 12, then “32 buckets”.
- Notice samosa moving from bucket 9 to bucket 25.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Group parcels by city
Problem statement
Parcels arrive as "id:city". Group parcel IDs by city with computeIfAbsent and print the groups in city order.
["P1:Pune", "P2:Mumbai", "P3:Pune", "P4:Thane"]
{Mumbai=[P2], Pune=[P1, P3], Thane=[P4]}Approach
- A
TreeMap<String, List<String>>so cities print sorted. - For each parcel,
computeIfAbsent(city, k -> new ArrayList<>())returns the city's list, creating it the first time. - Add the ID to that list.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Two items that use a gift card exactly
Problem statement
A customer has a ₹500 gift card. From prices [120, 260, 380, 240], find two different items whose prices add up to exactly 500, in one pass.
prices [120, 260, 380, 240], target 500
Items 0 and 2: ₹120 + ₹380
Approach
- Keep a map from price → index of items seen so far.
- For each price p, check whether
500 − pis already in the map. - If yes, report the pair; otherwise store p. O(n) instead of checking every pair.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 11.6 in one screen
- HashMap: key → value, unique keys, one null key, unordered, O(1) average.
- Inside:
Node[] table; node = hash, key, value, next. - put:
hash = h ^ (h >>> 16)→ index(n−1) & hash→ empty? insert : walk chain (hash + equals) → replace or append →size > threshold? resize. - Defaults 16 / 0.75 → resize at the 13th entry → 32 buckets; nodes go to index or index + oldCap.
- Chains → tree at 8 nodes (if capacity ≥ 64); back to list at 6.
- Needs correct, stable
equals/hashCode; never mutate keys. - Use:
getOrDefault,putIfAbsent,merge,computeIfAbsent,entrySet. Ordered: LinkedHashMap, TreeMap. Threads: ConcurrentHashMap.
Interview and viva questions
Explain how HashMap works internally.
It's an array of buckets. put computes the key's hash (hashCode with high bits spread), finds the bucket with (n − 1) & hash, and either inserts a new node, or walks the bucket's chain comparing hash and equals to replace an existing key's value or append a node. When size exceeds capacity × 0.75, the table doubles and nodes are split between index and index + old capacity. Long chains (8+) become red-black trees when the table has at least 64 buckets. get follows the same hash → bucket → equals path.
What happens if hashCode is good but equals is not overridden?
Equal-looking keys may land in the same bucket, but equals (identity) says they differ, so you get duplicates and failed lookups with a new key object. Both methods must be overridden consistently (Lesson 10.3).
Why is the capacity always a power of two?
So the index can be computed with a fast bit mask, (n − 1) & hash, instead of the slower %, and so that on resize each bucket splits cleanly into two (same index or index + old capacity) by checking a single bit.
HashMap vs Hashtable vs ConcurrentHashMap?
HashMap: not thread-safe, allows one null key and null values. Hashtable: legacy, every method synchronised, no nulls. ConcurrentHashMap: thread-safe with fine-grained locking/CAS, high concurrency, no null keys or values. Use ConcurrentHashMap for shared maps.
Why did Java 8 add treeification?
If many keys collide (poor hashCode or a deliberate attack), a chain could grow to thousands of nodes and make get/put O(n). Converting large buckets to red-black trees keeps the worst case at O(log n).
Practice problems
Use HashMap fluently and reason about its internals. The last three are business tasks.
Character frequency
Count how many times each character appears in "banana", printed in alphabetical order.
"banana"
{a=3, b=1, n=2}Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Character, Integer> freq = new TreeMap<>();
for (char c : "banana".toCharArray()) freq.merge(c, 1, Integer::sum);
System.out.println(freq);
}
}How it works: merge inserts 1 for a new key or adds 1 to the existing count.
Iterate entries
Print each state with its capital from a map of three states, one per line, in insertion order.
Maharashtra, Karnataka, Gujarat
Maharashtra → Mumbai Karnataka → Bengaluru Gujarat → Gandhinagar
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, String> cap = new LinkedHashMap<>();
cap.put("Maharashtra", "Mumbai");
cap.put("Karnataka", "Bengaluru");
cap.put("Gujarat", "Gandhinagar");
for (Map.Entry<String, String> e : cap.entrySet()) {
System.out.println(e.getKey() + " → " + e.getValue());
}
}
}How it works: entrySet() gives key and value together; LinkedHashMap keeps insertion order.
Compute the bucket index
Write the HashMap index formula and print the 16-bucket index of "Aa", "BB" and "chai".
"Aa", "BB", "chai"
Aa→0 BB→0 chai→3
Show solution and explanation
public class Main {
static int index(Object key, int n) {
int h = key.hashCode();
return (n - 1) & (h ^ (h >>> 16));
}
public static void main(String[] args) {
for (String k : new String[]{"Aa", "BB", "chai"}) System.out.print(k + "→" + index(k, 16) + " ");
System.out.println();
}
}How it works: "Aa" and "BB" have the same hash code (2112), so they always collide.
First non-repeating character
Find the first character in "swiss" that appears only once.
"swiss"
w
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
String s = "swiss";
Map<Character, Integer> count = new LinkedHashMap<>();
for (char c : s.toCharArray()) count.merge(c, 1, Integer::sum);
for (Map.Entry<Character, Integer> e : count.entrySet()) {
if (e.getValue() == 1) { System.out.println(e.getKey()); break; }
}
}
}How it works: a LinkedHashMap counts in one pass and remembers first-seen order for the second pass. O(n).
Invert a map
Invert {chai=15, vada=25, poha=15} into price → list of dishes, sorted by price.
{chai=15, vada=25, poha=15}{15=[chai, poha], 25=[vada]}Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> price = new LinkedHashMap<>();
price.put("chai", 15); price.put("vada", 25); price.put("poha", 15);
Map<Integer, List<String>> byPrice = new TreeMap<>();
for (Map.Entry<String, Integer> e : price.entrySet()) {
byPrice.computeIfAbsent(e.getValue(), k -> new ArrayList<>()).add(e.getKey());
}
System.out.println(byPrice);
}
}How it works: values may repeat, so the inverted map's values are lists.
Stock after orders
Stock: pen 10, book 4. Orders: pen 3, book 5, pen 2. Fulfil an order only if enough stock exists; print rejected orders and final stock.
3 orders
Rejected: book x5
{book=4, pen=5}Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> stock = new TreeMap<>(Map.of("pen", 10, "book", 4));
String[] item = {"pen", "book", "pen"};
int[] qty = {3, 5, 2};
for (int i = 0; i < item.length; i++) {
int have = stock.getOrDefault(item[i], 0);
if (have >= qty[i]) stock.put(item[i], have - qty[i]);
else System.out.println("Rejected: " + item[i] + " x" + qty[i]);
}
System.out.println(stock);
}
}How it works: each order is one O(1) lookup and one update.
Join orders with customers
Customers: (1, Riya), (2, Kabir). Orders: (101, cust 2), (102, cust 1), (103, cust 2). Print each order with the customer name, using a map instead of a nested loop.
2 customers, 3 orders
Order 101 → Kabir Order 102 → Riya Order 103 → Kabir
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Integer, String> customer = new HashMap<>();
customer.put(1, "Riya");
customer.put(2, "Kabir");
int[][] orders = {{101, 2}, {102, 1}, {103, 2}};
for (int[] o : orders) {
System.out.println("Order " + o[0] + " → " + customer.get(o[1]));
}
}
}How it works: building the map is O(c) and each lookup O(1), so the join is O(c + o) instead of O(c × o).
Rate-limit API keys
Each API key may make at most 3 calls. Process calls K1, K2, K1, K1, K1, K2 and print which are allowed.
K1, K2, K1, K1, K1, K2
K1 allowed (1) K2 allowed (1) K1 allowed (2) K1 allowed (3) K1 BLOCKED K2 allowed (2)
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> used = new HashMap<>();
for (String key : new String[]{"K1", "K2", "K1", "K1", "K1", "K2"}) {
int n = used.merge(key, 1, Integer::sum);
System.out.println(key + (n <= 3 ? " allowed (" + n + ")" : " BLOCKED"));
}
}
}How it works: merge returns the new count, so the check and update are one call. (A real limiter also resets counts per time window and uses ConcurrentHashMap.)
Real-world scenario questions (asked in company interviews)
HashMap is the single most asked Java collections topic. Interviewers love scenarios where the internals explain a production bug.
A cache that got slower as it grew
A product cache uses HashMap<ProductKey, Product>. Lookups were fast with 1,000 entries but take milliseconds with 100,000. Profiling shows time spent in ProductKey.equals. What's your hypothesis?
The interviewer is checking: the effect of a poor hashCode.
Show a strong answer
- Most keys probably share a few hash codes (e.g.
hashCodeuses only a category field, or returns a constant), so buckets hold long chains and each lookup calls equals many times. - Check bucket distribution in a test; fix
hashCodeto use all fields thatequalsuses (Objects.hash(...)) or make the key a record. - Treeification softens it to O(log n) only if keys are Comparable; a good hash is the real fix.
Avoid: increasing the initial capacity. More buckets don't help if every key hashes to the same value.
A booking that can't be found or removed
Bookings are stored in a HashMap<Seat, Booking>. After a passenger changes coach (the code updates seat.coach), map.get(seat) returns null and map.remove(seat) does nothing, but size() still counts it. Explain.
The interviewer is checking: mutable keys and bucket placement.
Show a strong answer
- The entry was placed by the old hash. Changing a field used in
hashCodechanges the hash, so lookups go to a different bucket. - Make
Seatimmutable (final fields, or a record). To change seat:remove(oldSeat), thenput(newSeat, booking). - Add a test that mutating keys is impossible (no setters).
Avoid: “just iterate the map to find it”. That turns O(1) into O(n) and hides the design flaw.
Lost counts under load
A shared HashMap<String, Integer> counts API calls per client from many request threads with map.put(k, map.getOrDefault(k, 0) + 1). Under load the totals are too low, and once a thread got stuck at 100% CPU. What's happening?
The interviewer is checking: HashMap isn't thread-safe.
Show a strong answer
- Read-modify-write races lose increments; concurrent resizes can corrupt the table (older JDKs could even form a cycle in a chain, causing infinite loops).
- Use
ConcurrentHashMapwithmerge(k, 1, Integer::sum), orConcurrentHashMap<String, LongAdder>for very hot counters. - Module 12 covers why races happen.
Avoid: Collections.synchronizedMap with the same get-then-put code; the compound action is still not atomic.
Loading 10 lakh customers into a map
A job builds a HashMap of 1,000,000 customers by ID. It's slower than expected and memory spikes during loading. What would you change?
The interviewer is checking: resizing cost and pre-sizing.
Show a strong answer
- Starting at 16 buckets, the map resizes about 17 times, each time allocating a doubled table and moving every node.
- Pre-size: capacity ≥ expected / 0.75 (e.g.
HashMap.newHashMap(1_000_000)on Java 19+, ornew HashMap<>(1_333_334)). - Consider primitive-keyed maps from a library if memory is critical, or stream the data instead of holding it all.
Avoid: new HashMap<>(1_000_000) thinking it holds a million without resizing; with load factor 0.75 it still resizes once.