Internal Structure
Internal Structure of HashMap
HashMap uses a hash table internally. It consists of an array of "buckets" (also called bins). Each bucket holds a linked list (or tree) of entries that hash to the same bucket index.
Key fields:
table— the array of buckets (Node[])size— number of key-value pairscapacity— length of the table array (always a power of 2)loadFactor— threshold for resizing (default 0.75)threshold— capacity * loadFactor
Java 8+ improvement: When a bucket has more than 8 entries, the linked list converts to a red-black tree for O(log n) lookup instead of O(n).
import java.util.*;
public class HashMapInternalDemo {
public static void main(String[] args) {
// Create HashMap
Map<String, Integer> map = new HashMap<>();
// Internal structure visualization
// table[0] -> null
// table[1] -> Entry("apple", 1) -> Entry("banana", 2) -> null
// table[2] -> null
// table[3] -> Entry("cherry", 3) -> null
// ...
// How hashing works
String key1 = "apple";
String key2 = "banana";
String key3 = "cherry";
System.out.println("apple hash: " + key1.hashCode());
System.out.println("banana hash: " + key2.hashCode());
System.out.println("cherry hash: " + key3.hashCode());
// Bucket index = hash & (capacity - 1)
// This is why capacity must be a power of 2
int capacity = 16; // default initial capacity
System.out.println("\napple bucket: " + (key1.hashCode() & (capacity - 1)));
System.out.println("banana bucket: " + (key2.hashCode() & (capacity - 1)));
System.out.println("cherry bucket: " + (key3.hashCode() & (capacity - 1)));
// Putting entries
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
System.out.println("\nMap: " + map);
// Default capacity and load factor
Map<Object, Object> defaultMap = new HashMap<>();
System.out.println("Default capacity: 16");
System.out.println("Default load factor: 0.75");
}
}
Key design decisions:
- Capacity is always a power of 2 for efficient index calculation
- Load factor of 0.75 balances space usage and collision probability
- Java 8+ uses treeification (red-black tree) for buckets with >8 entries
Core Methods
Core Methods of HashMap
HashMap provides methods for inserting, retrieving, removing, and checking for keys.
import java.util.*;
public class HashMapMethodsDemo {
public static void main(String[] args) {
// Creating
Map<String, Integer> scores = new HashMap<>();
// put - insert or update
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
scores.put("Alice", 98); // overwrites previous value
System.out.println("Map: " + scores);
// get - retrieve value by key
Integer aliceScore = scores.get("Alice");
System.out.println("Alice: " + aliceScore); // 98
// getOrDefault - return default if key not found
Integer daveScore = scores.getOrDefault("Dave", 0);
System.out.println("Dave: " + daveScore); // 0
// containsKey / containsValue
System.out.println("Has Alice? " + scores.containsKey("Alice")); // true
System.out.println("Has 95? " + scores.containsValue(95)); // false (was overwritten)
// remove
scores.remove("Bob");
scores.remove("Charlie", 92); // remove only if value matches
System.out.println("After removes: " + scores);
// replace
scores.replace("Alice", 100); // replace value
scores.putIfAbsent("Eve", 75); // only put if absent
System.out.println("After replace: " + scores);
// compute - transform value based on current value
scores.compute("Alice", (key, val) -> val == null ? 0 : val + 10);
System.out.println("After compute: " + scores);
// merge - combine values
Map<String, Integer> map1 = new HashMap<>(Map.of("a", 1, "b", 2));
Map<String, Integer> map2 = new HashMap<>(Map.of("b", 3, "c", 4));
map2.forEach((key, val) -> map1.merge(key, val, Integer::sum));
System.out.println("After merge: " + map1); // {a=1, b=5, c=4}
// keySet, values, entrySet
System.out.println("Keys: " + scores.keySet());
System.out.println("Values: " + scores.values());
System.out.println("Entries: " + scores.entrySet());
// iteration
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
}
Collision Handling
Hash Collision Handling
A hash collision occurs when two different keys produce the same bucket index. HashMap handles collisions using separate chaining (linked lists in each bucket).
Collision resolution in Java 8+:
- Each bucket starts as a linked list
- When a bucket has >8 entries, the list converts to a red-black tree (treeification)
- When a bucket shrinks below 6 entries, the tree converts back to a list (untreeification)
import java.util.*;
public class CollisionHandlingDemo {
// Custom class with intentional hash collisions
static class CollidingKey {
private int id;
public CollidingKey(int id) {
this.id = id;
}
@Override
public int hashCode() {
return 42; // All keys hash to same value!
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
CollidingKey other = (CollidingKey) obj;
return id == other.id;
}
@Override
public String toString() {
return "Key(" + id + ")";
}
}
public static void main(String[] args) {
// Demonstrate collision handling
Map<CollidingKey, String> map = new HashMap<>();
// All these keys have the same hashCode!
for (int i = 0; i < 20; i++) {
map.put(new CollidingKey(i), "Value " + i);
}
System.out.println("Map size: " + map.size()); // 20
System.out.println("All values still accessible:");
for (int i = 0; i < 20; i++) {
System.out.println(" " + map.get(new CollidingKey(i)));
}
// Good hashCode example
Map<String, Integer> goodMap = new HashMap<>();
for (int i = 0; i < 1000; i++) {
goodMap.put("key" + i, i);
}
// Even distribution across buckets
}
}
Best practices for hashCode():
- Use all significant fields in the hash calculation
- Distribute hash values evenly across the integer range
- If a.equals(b), then a.hashCode() == b.hashCode()
Load Factor and Resizing
Load Factor and Resizing
The load factor determines when HashMap resizes. When the number of entries exceeds capacity * loadFactor, HashMap creates a new table with double the capacity and rehashes all entries.
Default values:
- Initial capacity: 16
- Load factor: 0.75
- Threshold: 16 * 0.75 = 12 (resize after 12 entries)
import java.util.*;
public class LoadFactorDemo {
public static void main(String[] args) {
// Default: capacity 16, load factor 0.75
Map<Integer, Integer> defaultMap = new HashMap<>();
for (int i = 0; i < 12; i++) {
defaultMap.put(i, i * 10);
}
System.out.println("Default map (12 entries): capacity still 16");
System.out.println("Adding 13th entry triggers resize to 32");
defaultMap.put(12, 120);
// Custom load factor
Map<Integer, Integer> lowLoad = new HashMap<>(16, 0.5f);
for (int i = 0; i < 8; i++) {
lowLoad.put(i, i * 10);
}
System.out.println("\nLow load factor (0.5): resize after 8 entries");
// High load factor
Map<Integer, Integer> highLoad = new HashMap<>(16, 0.9f);
for (int i = 0; i < 14; i++) {
highLoad.put(i, i * 10);
}
System.out.println("High load factor (0.9): resize after 14 entries");
// Pre-sizing for performance
int expectedSize = 1000;
int initialCapacity = (int) (expectedSize / 0.75f) + 1;
Map<Integer, String> optimized = new HashMap<>(initialCapacity);
System.out.println("\nPre-sized for 1000 entries: initial capacity = " + initialCapacity);
// The resize process
System.out.println("\nResize process:");
System.out.println("1. Create new table with 2x capacity");
System.out.println("2. Rehash all entries into new table");
System.out.println("3. Discard old table");
}
}
Tradeoffs:
- Lower load factor (0.5): fewer collisions, more memory, more frequent resizing
- Higher load factor (0.9): more collisions, less memory, less frequent resizing
- 0.75 is the sweet spot for most use cases
Time Complexity
HashMap Time Complexity
| Operation | Average | Worst Case |
|---|---|---|
| put(key, value) | O(1) | O(n)* |
| get(key) | O(1) | O(n)* |
| remove(key) | O(1) | O(n)* |
| containsKey(key) | O(1) | O(n)* |
| containsValue(value) | O(n) | O(n) |
| size() | O(1) | O(1) |
| isEmpty() | O(1) | O(1) |
| iteration | O(capacity) | O(capacity) |
*Worst case O(n) occurs when all keys hash to the same bucket (bad hashCode).
With Java 8+ treeification, worst case is O(log n) for get/put/remove.
import java.util.*;
public class HashMapComplexityDemo {
public static void main(String[] args) {
// Average case: O(1)
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1); // O(1) average
int val = map.get("key1"); // O(1) average
map.remove("key1"); // O(1) average
// containsValue is always O(n)
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
boolean has = map.containsValue(2); // O(n) - must scan all values
// Iteration is O(capacity), not O(size)
Map<Integer, Integer> small = new HashMap<>(2);
small.put(1, 10);
small.put(2, 20);
// Internal array has 2 slots, iterating visits all 2
// Demonstrate O(1) performance
Map<Integer, Integer> large = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) {
large.put(i, i);
}
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
large.get(i); // O(1) each
}
long time = System.nanoTime() - start;
System.out.println("1M gets: " + time / 1_000_000 + "ms");
System.out.println("Average per get: " + time / 1_000_000.0 + "ns");
// When HashMap is slow
System.out.println("\nHashMap is slow when:");
System.out.println("1. Bad hashCode() causes many collisions");
System.out.println("2. Load factor too high (>0.9)");
System.out.println("3. containsValue() which scans all entries");
}
}
Practice Problems
Given an array of integers and a target, return indices of two numbers that add up to the target. Use a HashMap for O(n) solution.
Solution
import java.util.*;
public class TwoSum {
public static int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No solution found");
}
}Write a method that takes a string of words and returns a HashMap with each word and its frequency count. Words should be lowercase.
Solution
import java.util.*;
public class WordFrequency {
public static Map<String, Integer> countFrequency(String text) {
Map<String, Integer> freq = new HashMap<>();
String[] words = text.toLowerCase().split("\\\s+");
for (String word : words) {
freq.merge(word, 1, Integer::sum);
}
return freq;
}
}Given an array of strings, group anagrams together. Use a HashMap where the key is the sorted string and the value is the list of anagrams.
Solution
import java.util.*;
public class GroupAnagrams {
public static List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String sorted = new String(chars);
map.computeIfAbsent(sorted, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}
}Given a string, find the first character that does not repeat. Return the character or null if none exists. Use a HashMap to count frequencies.
Solution
import java.util.*;
public class FirstNonRepeating {
public static Character firstNonRepeating(String s) {
Map<Character, Integer> freq = new LinkedHashMap<>();
for (char c : s.toCharArray()) {
freq.merge(c, 1, Integer::sum);
}
for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
if (entry.getValue() == 1) return entry.getKey();
}
return null;
}
}Given a linked list, determine if it has a cycle using a HashSet to track visited nodes. Return true if a cycle exists.
Solution
import java.util.*;
public class CycleDetection {
public static boolean hasCycle(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode current = head;
while (current != null) {
if (visited.contains(current)) return true;
visited.add(current);
current = current.next;
}
return false;
}
static class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
}Quiz
1. What is the default load factor of HashMap?
2. What happens when a HashMap bucket has more than 8 entries in Java 8+?
3. What is the time complexity of containsValue() in HashMap?
4. When does HashMap resize?
5. Why should you override hashCode() and equals() together?
Flashcards
Question
What is the internal structure of HashMap?
Click to reveal answer
Answer
An array of buckets. Each bucket contains a linked list (or red-black tree in Java 8+ for >8 entries) of key-value pairs that hash to the same bucket index.
Question
What is the load factor in HashMap and what does it control?
Click to reveal answer
Answer
Load factor (default 0.75) determines when HashMap resizes. When entries exceed capacity * loadFactor, HashMap doubles capacity and rehashes all entries. Lower = less collisions, more memory.
Question
What is the average time complexity of HashMap.get()?
Click to reveal answer
Answer
O(1) average. The hash function maps keys directly to bucket indices. With a good hash function and proper load factor, most lookups are O(1). Worst case is O(log n) with treeification.
Question
What is hash collision and how does HashMap handle it?
Click to reveal answer
Answer
A collision occurs when two different keys map to the same bucket. HashMap uses separate chaining: each bucket holds a linked list. Java 8+ converts lists to red-black trees when bucket size >8.
Question
How do you pre-size a HashMap for a known number of entries?
Click to reveal answer
Answer
new HashMap<>((int)(expectedSize / 0.75f) + 1). This avoids costly resize operations when you know approximately how many entries you'll store.
Revision Notes
Key Takeaways
- 1.HashMap provides O(1) average time for get, put, and remove
- 2.Load factor controls when HashMap resizes (default 0.75)
- 3.Java 8+ uses red-black trees in buckets with >8 entries
- 4.Always override hashCode() and equals() for custom key types
- 5.Pre-size HashMap if you know the expected number of entries
Interview Tips
- •Explain how HashMap works internally: hash function -> bucket -> linked list/tree
- •Discuss the load factor tradeoff: lower = less collisions, more memory
- •Explain Java 8 treeification: why trees replace lists when bucket size >8
- •Know the contract: equals() must be consistent with hashCode()
Cheat Sheet
HashMap Cheat Sheet
Internal Structure
- Array of buckets (Node[])
- Each bucket: linked list -> red-black tree (Java 8+, >8 entries)
- Default capacity: 16, load factor: 0.75
- Bucket index: hash & (capacity - 1)
Key Methods
- put(k, v) / get(k) / remove(k) -> O(1) average
- containsKey(k) -> O(1)
- containsValue(v) -> O(n)
- merge(k, v, combiner) -> O(1)
Resizing
- Triggers when size > capacity * loadFactor
- Doubles capacity, rehashes all entries
- Cost: O(n) per resize
- Pre-size to avoid: new HashMap<>((int)(n/0.75f)+1)
Contract
- Override hashCode() and equals() together
- If a.equals(b) -> a.hashCode() == b.hashCode()