Made by VeraStack Labs
Your phone's contacts
Type a name, get a number.
Open the contacts app on your phone and type "Ra". Ravi's number appears. The phone did not read every contact from the top to find him. It went to the right place straight away.
A HashMap is that contact list. It stores pairs: a key (the name you search by) and a value (what you want back, such as a number). You give it a key, and it gives you the value, in about the same time whether it holds ten contacts or ten million. Each key can appear only once, so saving "Ravi" a second time replaces his number instead of adding a second Ravi.
A HashSet is the same machine with the values taken away. It keeps only the keys, so it answers one question very quickly: "have I seen this before?" Think of a guest list at a door.
These two are the most useful collections after ArrayList. They turn many slow "look through everything" loops into one quick lookup.
Remember: a HashMap finds a value from its key without searching. A HashSet is a HashMap that only cares about the keys.
Keys become buckets
A number picks the shelf, so no searching.
How can it find "Ravi" without searching? By turning the key into a shelf number.
Every Java object has a method called hashCode(). It returns an int that acts like a fingerprint of the object. For a String, the number is worked out from its letters by a fixed formula, so "Asha" always gives 2050251.
The map owns a row of buckets, which are numbered shelves. It squeezes the fingerprint into the range 0 to (number of buckets - 1), and that is the shelf. In the picture, "Asha" belongs on shelf 3. To find Asha again, the map repeats the sum, goes straight to shelf 3, and looks there. No other shelf is touched.
Two different keys can land on the same shelf. That is called a collision. Kiran and Divya do in the picture. It is not an error. The shelf simply keeps a short chain of everything that landed on it, and the map checks the items on that one shelf with equals, the method that asks "are these two the same?" As long as the chains stay short, a lookup stays quick. Keeping them short is why the map grows its row of buckets when it gets crowded.
Sets, maps, and counting
put, get, getOrDefault, merge, computeIfAbsent.
Every block below can be pasted into jshell. Start with a HashSet, the guest list.
Set<String> invited = new HashSet<>();
System.out.println(invited.add("Asha"));
System.out.println(invited.add("Ravi"));
System.out.println(invited.add("Asha")); // already there, so nothing changes
System.out.println(invited.contains("Ravi"));
System.out.println(invited.size());Paste into jshell to try it
true
true
false
true
2add answers true when it added something new, and false when the item was already there. That answer is useful on its own, and problem 1 uses it. A set never holds duplicates, so adding "Asha" twice leaves the size at 2.
Now the contact list, as a HashMap.
Map<String, String> contacts = new HashMap<>();
contacts.put("Asha", "98450 11111");
contacts.put("Ravi", "98450 22222");
System.out.println(contacts.get("Ravi"));
System.out.println(contacts.get("Meera")); // no such key: null, not an error
System.out.println(contacts.containsKey("Asha"));
System.out.println(contacts.put("Asha", "98450 33333")); // returns the old value
System.out.println(contacts.size());Paste into jshell to try it
98450 22222
null
true
98450 11111
2put(key, value) stores a pair. get(key) reads it back, and gives null when the key is missing. Be careful with that null: if your map holds Integer values and you write int n = map.get("x") for a missing key, Java tries to unbox null and throws a NullPointerException. Check with containsKey, or use one of the helpers below.
The last put replaced Asha's number, and handed back the old one. The size stayed at 2.
The counting problem
A very common job: count how many times each word appears. The first attempt reads the old count, adds one, and puts it back.
String[] words = {"tea", "chai", "tea", "coffee", "tea", "chai"};
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
// getOrDefault gives 0 for a word we have not met yet
counts.put(word, counts.getOrDefault(word, 0) + 1);
}
System.out.println(counts.get("tea"));
System.out.println(counts.get("chai"));
System.out.println(counts.get("coffee"));Paste into jshell to try it
3
2
1getOrDefault(word, 0) means "the count so far, or 0 if this word is new". It saves you an if. There is a shorter way still.
String[] words = {"tea", "chai", "tea", "coffee", "tea", "chai"};
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
// "put 1 if new, otherwise combine the old value with 1 using sum"
counts.merge(word, 1, Integer::sum);
}
System.out.println(counts.get("tea"));
System.out.println(counts.get("chai"));
System.out.println(counts.get("coffee"));Paste into jshell to try it
3
2
1merge(key, 1, Integer::sum) says: "if the key is new, store 1. If it already has a value, combine the old value and 1 with sum." The Integer::sum part is a method reference, which is a shorthand for passing an existing method as the rule. Counting is the use you will meet most.
Another common job: sort things into groups, such as names by their first letter. Each group is a list, and the list must exist before you can add to it.
String[] names = {"Asha", "Ravi", "Anil", "Meera", "Ravindra"};
Map<Character, List<String>> byInitial = new HashMap<>();
for (String name : names) {
// make the empty list only the first time a letter appears
byInitial.computeIfAbsent(name.charAt(0), letter -> new ArrayList<>()).add(name);
}
System.out.println(byInitial.get('A'));
System.out.println(byInitial.get('R'));
System.out.println(byInitial.get('M'));Paste into jshell to try it
[Asha, Anil]
[Ravi, Ravindra]
[Meera]computeIfAbsent(key, rule) looks for the key. If it is missing, it runs the rule to make the value (here, a new empty list), stores it, and returns it. If the key is there, it returns what is stored. Either way you get a list back and can call add on it straight away. The letter -> new ArrayList<>() is a lambda, and it only runs the first time each letter appears. Ask for a letter that no name starts with, and get gives you null, as it does for any missing key.
Last, walking over a map. entrySet() gives you every key-value pair.
Map<String, Integer> stock = new HashMap<>();
stock.put("pens", 12);
stock.put("pads", 5);
stock.put("tape", 3);
int total = 0;
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
total += entry.getValue();
}
System.out.println(total);
// The map promises no order, so sort the keys to print them the same way every time.
List<String> keys = new ArrayList<>(stock.keySet());
Collections.sort(keys);
for (String key : keys) {
System.out.println(key + " = " + stock.get(key));
}Paste into jshell to try it
20
pads = 5
pens = 12
tape = 3A HashMap makes no promise about the order of its pairs. They might come out in any order, and the order can differ between Java versions. That is why the code sorts the keys before printing. Never write code that depends on the order. If you want a fixed order, lesson 8 has the collections for that.
Remember: getOrDefault reads with a fallback. merge combines values. computeIfAbsent creates a value the first time a key shows up.
Build a tiny HashMap
An array of shelves and a short chain on each.
Here is a small map with the same idea as the real one. It is an array of buckets, and each bucket is a chain of nodes (small objects holding a key, a value and a link to the next node).
static class TinyHashMap<K, V> {
private static class Node<K, V> {
final K key;
V value;
Node<K, V> next; // the next node in the same bucket
Node(K key, V value, Node<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
private Node<K, V>[] buckets = newBuckets(8);
private int size = 0;
@SuppressWarnings("unchecked")
private static <K, V> Node<K, V>[] newBuckets(int count) {
return (Node<K, V>[]) new Node<?, ?>[count];
}
int size() {
return size;
}
int bucketCount() {
return buckets.length;
}
// hashCode can be negative, so floorMod keeps the answer between 0 and length - 1
static int bucketFor(Object key, int bucketCount) {
return Math.floorMod(key.hashCode(), bucketCount);
}
V get(K key) {
for (Node<K, V> n = buckets[bucketFor(key, buckets.length)]; n != null; n = n.next) {
if (n.key.equals(key)) {
return n.value;
}
}
return null;
}
void put(K key, V value) {
int index = bucketFor(key, buckets.length);
for (Node<K, V> n = buckets[index]; n != null; n = n.next) {
if (n.key.equals(key)) {
n.value = value; // same key: replace, never add a second one
return;
}
}
buckets[index] = new Node<>(key, value, buckets[index]);
size++;
// A crowded table means long chains, so grow before it gets slow.
// 0.75 is the load factor the real HashMap also starts with.
if (size > buckets.length * 0.75) {
grow();
}
}
private void grow() {
Node<K, V>[] old = buckets;
buckets = newBuckets(old.length * 2);
for (Node<K, V> head : old) {
for (Node<K, V> n = head; n != null; ) {
Node<K, V> after = n.next; // save it, the move below overwrites next
int index = bucketFor(n.key, buckets.length);
n.next = buckets[index];
buckets[index] = n;
n = after;
}
}
}
}Paste into jshell to try it
Go through it in order.
bucketsstarts with 8 shelves. The realHashMapstarts with 16.bucketForturns a key into a shelf.hashCode()can be negative, and a negative number is not a valid shelf.Math.floorModalways answers from 0 up, so it is safe.getgoes straight to one shelf and walks only that chain, comparing keys withequals. A miss givesnull.putalso walks the chain first. If the key is already there it replaces the value. If not, it adds a new node at the front of the chain and counts it.growdoubles the number of shelves once the map is more than three-quarters full. That fraction is called the load factor, and the realHashMapstarts with the same 0.75. Every node must be moved to its new shelf, because the shelf number depends on how many shelves exist. We saven.nextbefore moving each node, since moving it overwrites the link.
System.out.println("Asha".hashCode() + " -> bucket " + TinyHashMap.bucketFor("Asha", 8));
System.out.println("Ravi".hashCode() + " -> bucket " + TinyHashMap.bucketFor("Ravi", 8));
TinyHashMap<String, String> contacts = new TinyHashMap<>();
contacts.put("Asha", "98450 11111");
contacts.put("Ravi", "98450 22222");
contacts.put("Asha", "98450 33333"); // replaces, so the size stays 2
System.out.println(contacts.get("Asha"));
System.out.println(contacts.size());Paste into jshell to try it
2050251 -> bucket 3
2539842 -> bucket 2
98450 33333
2Asha's key gave shelf 3 and Ravi's gave shelf 2, matching the picture. Saving Asha twice kept the size at 2. The numbers come from the formula Java documents for String, so you will see the same ones.
A HashSet is built on a HashMap and stores each item as a key. That is the whole trick.
What lookups cost
n is the number of items stored.
| Operation | Time | Why |
|---|---|---|
put, get, containsKey | O(1) on average | One shelf is chosen, and its chain is short |
remove(key) | O(1) on average | Same shelf lookup, then unlink |
HashSet add, contains, remove | O(1) on average | It is a HashMap underneath |
containsValue(v) | O(n) | Values have no shelves, so every pair is checked |
| Walking all pairs | O(n + shelves) | Every shelf is visited, even empty ones |
| Growing the table | O(n), but rare | Every node moves to a new shelf |
| Space used | O(n) | One node per pair, plus the row of shelves |
"On average" is doing real work here. If many keys land on the same shelf, that chain gets long and lookups slow down. Good hashCode methods spread keys out, and lesson 7 shows what goes wrong when one does not.
Three problems, fast lookups
Duplicates, pairs, and groups.
Problem 1
The first repeat
Write firstDuplicate, which returns the first word that has already appeared earlier in the list, or null if every word is different. For ["tea", "chai", "tea", "chai"] it returns "tea", because the second "tea" is the first word to repeat.
Start here
static String firstDuplicate(List<String> words) {
throw new UnsupportedOperationException("return the first word that has already appeared, or null");
}Paste into jshell to try it
Stuck?
Show a solution
static String firstDuplicate(List<String> words) {
Set<String> seen = new HashSet<>();
for (String word : words) {
if (!seen.add(word)) { // add says false when the word was already there
return word;
}
}
return null;
}Paste into jshell to try it
add does the check and the insert in one call, so there is no need for a separate contains. Each word costs O(1) on average, so the whole walk is O(n) time and O(n) space. The slow alternative compares every word with every earlier word, which is O(n squared). Returning at the first repeat means a long list may not be read to the end.
Problem 2
Two numbers that add up
Write twoSum, which finds two different positions whose numbers add up to target, and returns them as {earlier, later}. For {2, 7, 11, 15} and target 9 it returns {0, 1}. If no pair works, return an empty array. You may assume at most one pair works.
Start here
static int[] twoSum(int[] numbers, int target) {
throw new UnsupportedOperationException("find two positions whose numbers add up to target");
}Paste into jshell to try it
Stuck?
Show a solution
static int[] twoSum(int[] numbers, int target) {
Map<Integer, Integer> positionOf = new HashMap<>(); // number -> where we saw it
for (int i = 0; i < numbers.length; i++) {
Integer partner = positionOf.get(target - numbers[i]);
if (partner != null) {
return new int[] {partner, i};
}
positionOf.put(numbers[i], i); // after the check, so a number cannot pair with itself
}
return new int[0];
}Paste into jshell to try it
At each step you ask the map 'have I already passed the number that completes this one?' That is an O(1) lookup instead of a walk through the rest of the array, so the whole job is one pass: O(n) time and O(n) space. Storing the current number after the check is what stops {3} with target 6 from pairing with itself, while {3, 3} still works because the first 3 is stored by the time the second arrives. The map keeps the position as the value, which is the whole reason this needs a map rather than a set.
Problem 3
Group by length
Write groupByLength, which returns a map from each word length to the list of words with that length, in the order they appeared. For ["pen", "cup", "bottle", "ink"] it gives 3 mapped to [pen, cup, ink] and 6 mapped to [bottle]. An empty list gives an empty map.
Start here
static Map<Integer, List<String>> groupByLength(List<String> words) {
throw new UnsupportedOperationException("map each length to the words that have it");
}Paste into jshell to try it
Stuck?
Show a solution
static Map<Integer, List<String>> groupByLength(List<String> words) {
Map<Integer, List<String>> groups = new HashMap<>();
for (String word : words) {
groups.computeIfAbsent(word.length(), length -> new ArrayList<>()).add(word);
}
return groups;
}Paste into jshell to try it
computeIfAbsent makes the empty list the first time a length is seen, and returns the existing list every time after. Chaining .add(word) on its result handles both cases with no if. Words keep their original order inside each group, because each is appended as it is met. The walk is O(n), and each lookup is O(1) on average. Without computeIfAbsent you would write get, a null check, a put, and then the add.
Hashing in the interview
What to ask, and what to say.
HashMap is probably the most asked-about class in Java interviews. These are the usual questions.
- "How does a
HashMapwork?" It has an array of buckets.hashCode()of the key picks a bucket, andequalsfinds the right entry inside it. Say "buckets", "hashCode", "equals" and "collision" in that order and you have most of the answer. - "What is a collision, and what happens?" Two different keys land in the same bucket. Both are kept in that bucket and told apart with
equals. Lookups slow down only if the chains get long. - "What are the capacity and load factor?" The capacity is the number of buckets, and 16 to start. The load factor, 0.75 by default, says how full the map may get before it doubles the capacity and moves every entry.
- "Can a key be
null?" In aHashMap, yes, onenullkey. Do not rely on it. Some other maps refusenullkeys. - "Does a
HashMapkeep order?" No, and never depend on it. A fixed order needs aLinkedHashMapor aTreeMap, which lesson 8 covers. - "
HashMaporHashSet?" A map if you need to look something up by key. A set if you only need to ask "is this in there?" or want to remove duplicates. - Count words, or find two numbers that add up. Problems 2 and 3 are the standard answers. Say the cost: one pass, O(n) time, O(n) extra space.
- Say the cost. "Lookup, insert and remove are O(1) on average. That is an average and not a promise, because it depends on the keys spreading out."
Updated 2026-10-17
Made by VeraStack Labs