Java HashMap
A complete guide to Java HashMap — internal bucket-array hashing, Java 8 treeification, all Map methods, null key and value handling, load factor and rehashing, HashMap vs Hashtable vs LinkedHashMap vs TreeMap, thread-safety considerations, and best practices with real-world examples.
Last Updated
March 2026
Read Time
19 min
Level
Beginner to Intermediate
What is Java HashMap?
Java HashMap is the most widely used implementation of the Map<K, V> interface in the Java Collections Framework. Introduced in Java 1.2, it stores data as key-value pairs inside an internal array of buckets (nodes), using a hash function to compute where each entry belongs. It provides O(1) average-case time complexity for get() and put() operations — making it the default go-to Map for everyday Java development.
Unlike its ancestor Hashtable, HashMap is not synchronized and therefore not inherently thread-safe. It is designed for single-threaded use or scenarios where external synchronization is managed by the developer. This deliberate design choice eliminates lock overhead and makes it significantly faster than Hashtable in single-threaded workloads.
HashMap stands apart from other Map implementations with two key permissions: it allows exactly one null key (always stored at bucket index 0) and any number of null values. It does not maintain insertion order (use LinkedHashMap for that) and does not sort keys (use TreeMap for that). Iteration order is undefined and can change after a rehash.
A landmark improvement came in Java 8: when a single bucket's linked-list chain exceeds 8 nodes (TREEIFY_THRESHOLD) and the overall table capacity is at least 64, that chain is automatically converted into a Red-Black Tree. This brings worst-case lookup for hash-colliding keys from O(n) down to O(log n) — a critical fix for adversarial inputs designed to create hash collisions.
Internal Structure — How HashMap Works Under the Hood
HashMap internally maintains an array of Node objects. Each Node is the head of a singly linked list — or, in Java 8+, a Red-Black Tree — of entries that map to the same bucket index. This collision-resolution strategy is called separate chaining.
// Simplified representation of HashMap's internal Node (Java 8+)
static class Node<K, V> implements Map.Entry<K, V> {
final int hash; // cached hash of the key
final K key; // the key — may be null (stored at index 0)
V value; // the value — may be null
Node<K,V> next; // next node in the same bucket chain
}
// When chain length > TREEIFY_THRESHOLD (8) AND table.length >= 64:
// Node<K,V> is promoted to TreeNode<K,V> — a Red-Black Tree node
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
TreeNode<K,V> parent, left, right, prev;
boolean red;
}
// Core internal state of HashMap:
transient Node<K,V>[] table; // the bucket array (length always power of two)
transient int size; // total number of key-value entries
int threshold; // size at which rehash occurs (capacity × loadFactor)
final float loadFactor; // default 0.75f
// Key constants:
static final int DEFAULT_INITIAL_CAPACITY = 16; // must be power of two
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8; // chain → tree
static final int UNTREEIFY_THRESHOLD = 6; // tree → chain
static final int MIN_TREEIFY_CAPACITY = 64; // table must be this big to treeifyHow put(key, value) works step by step:
Step 1: Null check → if (key == null) → store at index 0 (special path)
Step 2: Hash spreading → hash = key.hashCode() ^ (key.hashCode() >>> 16)
(XOR high bits into low bits to improve distribution)
Step 3: Find bucket index → index = hash & (table.length - 1) (fast bitwise AND)
Step 4: Check bucket chain → scan linked list / tree for key using equals()
If key found → update value, return old value
If not found → append new Node to chain / insert into tree
Step 5: Check treeify → if chain length ≥ TREEIFY_THRESHOLD (8)
AND table.length ≥ MIN_TREEIFY_CAPACITY (64)
→ convert chain to Red-Black Tree
Step 6: Check threshold → if ++size > threshold → resize() [double capacity]
Bucket Array (capacity = 16):
┌──────┐
│ 0 │ → [null key entry] ← only null key goes here
│ 1 │ → null
│ 2 │ → ["city"="Mumbai"] → null
│ 3 │ → null
│ 4 │ → ["name"="Ravi"] → ["dept"="Finance"] → null ← chain (collision)
│ 5 │ → null
│ 6 │ → ["id"="201"] → null
│ ... │
│ 15 │ → null
└──────┘
After Java 8 treeification (if bucket 4 chain grows > 8 AND table.length >= 64):
│ 4 │ → [Red-Black Tree root] → O(log n) lookup instead of O(n)Why power-of-two capacity? HashMap always keeps its bucket array length as a power of two (16, 32, 64, …). This allows the bucket index to be computed with an ultra-fast bitwise AND — index = hash & (capacity - 1) — instead of the slower modulo operation (%) used by Hashtable. The hash spreading step (h ^ (h >>> 16)) compensates for the fact that low-bit patterns in power-of-two tables can cause clustering with poor hashCode() implementations.
HashMap Class Hierarchy
HashMap sits cleanly within the modern Java Collections Framework hierarchy, extending AbstractMap and implementing the Map interface — with no legacy baggage unlike Hashtable.
Notable subclass: java.util.LinkedHashMap extends HashMap and adds a doubly-linked list that threads through all entries, preserving either insertion order (default) or access order (when constructed with accessOrder=true). LinkedHashMap is the backbone of LRU cache implementations in Java.
Creating a HashMap in Java
HashMap provides four constructors. Choosing the right one — especially pre-sizing for a known workload — can eliminate unnecessary rehashing and improve performance.
import java.util.*;
public class CreateHashMap {
public static void main(String[] args) {
// 1. Default constructor: capacity=16, loadFactor=0.75
HashMap<String, Integer> hm1 = new HashMap<>();
// 2. Specify initial capacity
// Useful when approximate size is known in advance
HashMap<String, String> hm2 = new HashMap<>(50);
// 3. Specify both initial capacity and load factor
// loadFactor=0.6 → rehash at 60% fill (fewer collisions, more memory)
HashMap<String, Double> hm3 = new HashMap<>(100, 0.6f);
// 4. Initialize from an existing Map
Map<String, Integer> source = new Hashtable<>();
source.put("Java", 1);
source.put("Kotlin", 2);
HashMap<String, Integer> hm4 = new HashMap<>(source);
System.out.println("From map: " + hm4);
// Best practice: declare as Map interface reference
Map<String, String> config = new HashMap<>();
config.put("host", "localhost");
config.put("port", "9090");
config.put("env", "production");
System.out.println("Config: " + config);
// ✅ null key is allowed — stored at bucket index 0
hm1.put(null, 99);
System.out.println("Null key value: " + hm1.get(null));
// ✅ null value is allowed
hm1.put("unset", null);
System.out.println("Null value for 'unset': " + hm1.get("unset"));
}
}Output
From map: {Kotlin=2, Java=1} Config: {env=production, port=9090, host=localhost} Null key value: 99 Null value for 'unset': nullHashMap Methods — Complete Reference
HashMap implements the full Map interface including all Java 8+ default methods. None of the methods listed below are synchronized — HashMap is intended for single-threaded use.
import java.util.*;
public class HashMapMethods {
public static void main(String[] args) {
HashMap<String, Integer> population = new HashMap<>();
// put() — add entries
population.put("Mumbai", 20_667_000);
population.put("Delhi", 32_941_000);
population.put("Bangalore", 13_193_000);
population.put("Chennai", 7_088_000);
System.out.println("Cities: " + population);
// get()
System.out.println("Delhi population: " + population.get("Delhi"));
// containsKey() and containsValue()
System.out.println("Has Mumbai: " + population.containsKey("Mumbai"));
System.out.println("Pop 7088000 exists: " + population.containsValue(7_088_000));
// getOrDefault() — Java 8
System.out.println("Pune: " + population.getOrDefault("Pune", -1));
// putIfAbsent() — Java 8
population.putIfAbsent("Delhi", 0); // Delhi exists — no change
population.putIfAbsent("Pune", 7_764_000); // Pune is new — inserted
System.out.println("After putIfAbsent: Delhi=" + population.get("Delhi")
+ ", Pune=" + population.get("Pune"));
// replace()
population.replace("Chennai", 7_200_000);
System.out.println("Updated Chennai: " + population.get("Chennai"));
// compute() — Java 8: add 500_000 to Bangalore's population
population.compute("Bangalore", (city, pop) -> pop + 500_000);
System.out.println("Computed Bangalore: " + population.get("Bangalore"));
// merge() — Java 8: useful for accumulating values
population.merge("Mumbai", 1_000_000, Integer::sum);
System.out.println("Merged Mumbai: " + population.get("Mumbai"));
// remove()
population.remove("Pune");
System.out.println("After remove Pune: " + population.containsKey("Pune"));
// forEach — Java 8
System.out.println("--- All Cities ---");
population.forEach((city, pop) ->
System.out.printf("%-12s → %,d%n", city, pop));
}
}Output
Cities: {Chennai=7088000, Bangalore=13193000, Mumbai=20667000, Delhi=32941000} Delhi population: 32941000 Has Mumbai: true Pop 7088000 exists: true Pune: -1 After putIfAbsent: Delhi=32941000, Pune=7764000 Updated Chennai: 7200000 Computed Bangalore: 13693000 Merged Mumbai: 21667000 After remove Pune: false --- All Cities --- Chennai → 7,200,000 Bangalore → 13,693,000 Mumbai → 21,667,000 Delhi → 32,941,000Load Factor and Rehashing in HashMap
Two concepts directly govern HashMap's performance characteristics: load factor and rehashing. Getting these right — especially for known-size use cases — can dramatically reduce memory allocations and GC pressure in production applications.
- ▶
📐 Load Factor — A float between 0.0 and 1.0 that determines the fill-ratio at which the bucket array is resized. Default is 0.75f. Formula: threshold = capacity × loadFactor. With defaults: threshold = 16 × 0.75 = 12. The 13th insertion triggers rehashing.
- ▶
🔄 Rehashing (resize) — When size exceeds threshold, HashMap creates a new bucket array exactly 2× the current capacity (e.g., 16 → 32 → 64 → …) — always a power of two. Every existing entry is re-inserted into the new array using the new index calculation. Rehashing is O(n). Unlike Hashtable, HashMap rehashing is single-threaded (no lock) and the new capacity formula is simply oldCapacity << 1.
- ▶
📉 Low Load Factor (e.g., 0.25) — Table stays sparse → very few collisions → fast lookups. Trade-off: higher memory usage and more frequent rehashing. Use when access speed is critical and memory is plentiful.
- ▶
📈 High Load Factor (e.g., 0.90) — Table stays dense → memory-efficient. Trade-off: more collisions → longer chains → slower lookups. Treeification kicks in for the worst buckets, but overall throughput still degrades.
- ▶
✅ Pre-sizing Best Practice — If the final map size is known, construct with: new HashMap<>(expectedSize / 0.75 + 1). This sets the initial capacity high enough that no rehashing occurs, avoiding the O(n) resize cost entirely. Example: for 100 entries → new HashMap<>(135).
import java.util.*;
public class HashMapLoadFactor {
public static void main(String[] args) {
// Strategy 1: Default — rehashing may occur
HashMap<String, String> defaultMap = new HashMap<>();
// Threshold = 16 × 0.75 = 12 → rehashes at 13th entry
// Strategy 2: Pre-sized — no rehashing for up to 1000 entries
// initialCapacity = 1000 / 0.75 + 1 = 1335 (rounded to next power of two: 2048)
HashMap<String, String> preSeized = new HashMap<>(1335);
// Strategy 3: High-speed cache — sparse table, fewer collisions
HashMap<String, String> fastCache = new HashMap<>(256, 0.5f);
// Populate pre-sized map
for (int i = 1; i <= 1000; i++) {
preSeized.put("key-" + i, "value-" + i);
}
System.out.println("Pre-sized entries: " + preSeized.size());
System.out.println("No rehashing occurred — table pre-sized correctly.");
}
}Output
Pre-sized entries: 1000 No rehashing occurred — table pre-sized correctly.HashMap vs Hashtable vs LinkedHashMap vs TreeMap
Choosing the right Map implementation is one of the most impactful design decisions in Java. Here is a complete comparison to help you choose correctly every time.
Decision Guide: Default to HashMap for all single-threaded Map needs. Choose LinkedHashMap when iteration order matters (e.g., building an LRU cache). Choose TreeMap when keys need to be sorted or you need range operations like subMap(), headMap(), or tailMap(). For multi-threaded scenarios, use ConcurrentHashMap (unordered) or ConcurrentSkipListMap (sorted). Avoid Hashtable in all new code.
Null Key and Null Value Handling in HashMap
One of HashMap's most distinctive features is its deliberate support for null keys and null values. Understanding exactly how this works — and where it can cause subtle bugs — is important for every Java developer.
import java.util.*;
public class HashMapNullDemo {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
// ✅ One null key — always stored at bucket index 0
map.put(null, "default config");
System.out.println("null key → " + map.get(null));
// Overwrite null key (same as any other key)
map.put(null, "updated config");
System.out.println("null key (overwritten) → " + map.get(null));
// ✅ Multiple null values
map.put("primaryColor", null);
map.put("secondaryColor", null);
map.put("accentColor", "#FF5733");
System.out.println("primaryColor → " + map.get("primaryColor"));
// ⚠️ TRAP: get() returns null for BOTH absent keys AND null-value keys
// Use containsKey() to distinguish them:
System.out.println("get missing key: " + map.get("missingKey")); // null
System.out.println("get null-value key: " + map.get("primaryColor")); // null
// Both return null — but the meaning is different!
// Correct approach: containsKey() to distinguish
String key = "primaryColor";
if (map.containsKey(key)) {
System.out.println(key + " is set (value may be null): " + map.get(key));
} else {
System.out.println(key + " does not exist in map");
}
// getOrDefault() avoids null-value ambiguity for absent keys
System.out.println("font: " + map.getOrDefault("font", "Arial"));
}
}Output
null key → default config null key (overwritten) → updated config primaryColor → null get missing key: null get null-value key: null primaryColor is set (value may be null): null font: ArialThe null ambiguity trap: get(key) returns null when (a) the key is absent, or (b) the key is present but mapped to a null value. These two cases are indistinguishable from the return value alone. Always use containsKey() when you need to tell them apart, or prefer getOrDefault() to guarantee a non-null fallback for missing keys.
Iterating Over a HashMap
HashMap supports multiple iteration styles. Always prefer entrySet() or forEach — they are both more efficient and more readable than keySet()-based iteration (which requires a get() lookup for each key).
import java.util.*;
import java.util.stream.*;
public class IterateHashMap {
public static void main(String[] args) {
HashMap<String, Double> gpa = new HashMap<>();
gpa.put("Arjun", 8.9);
gpa.put("Sneha", 9.4);
gpa.put("Vikram", 7.8);
gpa.put("Pooja", 9.1);
// 1. entrySet() with for-each — RECOMMENDED: accesses both key and value
System.out.println("--- entrySet ---");
for (Map.Entry<String, Double> entry : gpa.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
// 2. keySet() — only when keys alone are needed
System.out.println("--- keySet ---");
for (String name : gpa.keySet()) {
System.out.println(name + " : " + gpa.get(name));
}
// 3. values() — iterate values only
System.out.println("--- values ---");
for (double score : gpa.values()) {
System.out.println(score);
}
// 4. forEach with lambda (Java 8+) — cleanest
System.out.println("--- forEach lambda ---");
gpa.forEach((name, score) ->
System.out.printf("%-8s GPA: %.1f%n", name, score));
// 5. Stream API — filter and process
System.out.println("--- Stream: GPA > 9.0 ---");
gpa.entrySet().stream()
.filter(e -> e.getValue() > 9.0)
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(e -> System.out.println(e.getKey() + " → " + e.getValue()));
// ⚠️ HashMap's Iterator is FAIL-FAST.
// Modifying the map during iteration throws ConcurrentModificationException.
// Safe removal during iteration: use Iterator.remove()
System.out.println("--- Safe removal during iteration ---");
Iterator<Map.Entry<String, Double>> it = gpa.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Double> e = it.next();
if (e.getValue() < 8.0) {
it.remove(); // safe — removes from underlying map
System.out.println("Removed: " + e.getKey());
}
}
System.out.println("After removal: " + gpa);
}
}HashMap put() Operation Flow — Flowchart
The flowchart below shows the complete decision path Java follows when put(key, value) is called on a HashMap, including the Java 8 treeification step.
Code Execution Flow — from source to output
HashMap with Generics and Custom Objects
HashMap is fully generic and works with any object as key or value. When using a custom class as a key, you must correctly override both hashCode() and equals(). Failing to do so is one of the most common — and hardest-to-debug — bugs in Java Map usage.
import java.util.*;
class EmployeeId {
private final String department;
private final int empNumber;
EmployeeId(String department, int empNumber) {
this.department = department;
this.empNumber = empNumber;
}
// MUST override equals — HashMap uses this to find keys in bucket chains
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof EmployeeId)) return false;
EmployeeId other = (EmployeeId) o;
return empNumber == other.empNumber
&& department.equals(other.department);
}
// MUST override hashCode — determines which bucket the key lands in
// Contract: if a.equals(b) then a.hashCode() == b.hashCode()
@Override
public int hashCode() {
return Objects.hash(department, empNumber);
}
@Override
public String toString() {
return department + "-" + empNumber;
}
}
public class HashMapCustomKey {
public static void main(String[] args) {
HashMap<EmployeeId, String> directory = new HashMap<>();
directory.put(new EmployeeId("TECH", 1001), "Ananya Sharma");
directory.put(new EmployeeId("TECH", 1002), "Rohan Mehta");
directory.put(new EmployeeId("HR", 2001), "Kavya Reddy");
// Lookup with a NEW EmployeeId object — same field values
// Succeeds ONLY because hashCode() and equals() are correctly overridden
String name = directory.get(new EmployeeId("TECH", 1001));
System.out.println("Found: " + name);
// Without overriding hashCode/equals, this would return null
// because two different object instances would land in different buckets
directory.forEach((id, empName) ->
System.out.println(id + " => " + empName));
}
}Output
Found: Ananya Sharma HR-2001 => Kavya Reddy TECH-1002 => Rohan Mehta TECH-1001 => Ananya SharmaBest Practices for Using HashMap
Following these practices ensures that your HashMap usage is correct, performant, and maintainable — whether in single-threaded utilities or complex enterprise applications.
- ▶
✅ 1. Declare as Map Interface Reference — Always declare variables as Map<K,V> rather than HashMap<K,V>. This decouples your code from the implementation and allows swapping to LinkedHashMap, TreeMap, or ConcurrentHashMap with a single line change: Map<String, Integer> scores = new HashMap<>();
- ▶
✅ 2. Pre-Size for Known Workloads — If the approximate number of entries is known, use: new HashMap<>(expectedSize / 0.75 + 1). This prevents rehashing entirely, saving O(n) resize operations and reducing GC pressure. For 1000 expected entries: new HashMap<>(1335).
- ▶
✅ 3. Always Override hashCode() and equals() for Custom Keys — Both methods must be consistent: if a.equals(b) is true, then a.hashCode() must equal b.hashCode(). Using IDE-generated or Objects.hash() implementations is recommended. Never use mutable fields in hashCode() computation for keys — changing a key's state after insertion makes it unretrievable.
- ▶
✅ 4. Use getOrDefault() Instead of get() + null Check — Prefer map.getOrDefault(key, defaultValue) over if (map.get(key) != null). The former is cleaner and handles the null-value ambiguity case correctly. For maps that may hold null values, use containsKey() to check existence.
- ▶
✅ 5. Use computeIfAbsent() for Lazy Initialization — Instead of checking containsKey() then calling put(), use computeIfAbsent(key, k -> new ArrayList<>()). It is atomic enough for single-threaded code and expresses intent more clearly. Example: map.computeIfAbsent("group", k -> new ArrayList<>()).add(item);
- ▶
✅ 6. Iterate with entrySet() or forEach — Never iterate with keySet() and call get() for each key — that's two lookups per entry. Use entrySet() or forEach(BiConsumer) which provides direct access to both key and value in a single traversal.
- ▶
❌ 7. Do Not Use HashMap in Multi-Threaded Code Without Synchronization — HashMap is not thread-safe. Concurrent modifications can cause infinite loops during rehash, data loss, or ConcurrentModificationException. Use ConcurrentHashMap for shared state between threads — never Collections.synchronizedMap(new HashMap<>()) which suffers the same coarse-locking problem as Hashtable.
- ▶
❌ 8. Do Not Use Mutable Objects as HashMap Keys — If a key object's fields change after insertion, its hashCode() changes, placing it in a different bucket. The original entry becomes permanently unreachable (memory leak). Always use immutable objects — String, Integer, enums, or custom immutable classes — as HashMap keys.
Real-World Code Examples
Example 1 — Word Frequency Counter
import java.util.*;
import java.util.stream.*;
public class WordFrequency {
public static void main(String[] args) {
String text = "the quick brown fox jumps over the lazy dog " +
"the dog barked at the fox and the fox ran away";
HashMap<String, Integer> freq = new HashMap<>();
// Count frequencies using merge() — cleanest approach
for (String word : text.split("\\s+")) {
freq.merge(word, 1, Integer::sum);
}
System.out.println("All word frequencies:");
freq.forEach((word, count) ->
System.out.printf(" %-10s : %d%n", word, count));
// Top 3 most frequent words using Stream API
System.out.println("\nTop 3 words:");
freq.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(3)
.forEach(e ->
System.out.printf(" %-10s : %d times%n", e.getKey(), e.getValue()));
}
}Output
All word frequencies: the : 5 fox : 3 dog : 2 quick : 1 brown : 1 jumps : 1 over : 1 lazy : 1 barked : 1 at : 1 and : 1 ran : 1 away : 1 Top 3 words: the : 5 times fox : 3 times dog : 2 timesExample 2 — Grouping Data with computeIfAbsent()
import java.util.*;
public class GroupByDepartment {
record Student(String name, String dept, double gpa) {}
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Arjun", "CS", 8.9),
new Student("Sneha", "ECE", 9.2),
new Student("Vikram", "CS", 7.8),
new Student("Pooja", "MECH", 8.5),
new Student("Rahul", "ECE", 8.1),
new Student("Priya", "CS", 9.5),
new Student("Aditya", "MECH", 7.3)
);
// Group students by department — computeIfAbsent avoids null check + put
HashMap<String, List<String>> byDept = new HashMap<>();
for (Student s : students) {
byDept.computeIfAbsent(s.dept(), k -> new ArrayList<>())
.add(s.name() + " (" + s.gpa() + ")");
}
// Print grouped results
System.out.println("Students by Department:");
byDept.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> {
System.out.println(" " + e.getKey() + ":");
e.getValue().forEach(s -> System.out.println(" → " + s));
});
// Average GPA per department using merge()
HashMap<String, Double> totalGpa = new HashMap<>();
HashMap<String, Integer> deptCount = new HashMap<>();
for (Student s : students) {
totalGpa.merge(s.dept(), s.gpa(), Double::sum);
deptCount.merge(s.dept(), 1, Integer::sum);
}
System.out.println("\nAverage GPA by Department:");
totalGpa.forEach((dept, total) ->
System.out.printf(" %-6s : %.2f%n", dept, total / deptCount.get(dept)));
}
}Output
Students by Department: CS: → Arjun (8.9) → Vikram (7.8) → Priya (9.5) ECE: → Sneha (9.2) → Rahul (8.1) MECH: → Pooja (8.5) → Aditya (7.3) Average GPA by Department: CS : 8.73 ECE : 8.65 MECH : 7.90Practice This Code — Live Editor
Advantages and Disadvantages of HashMap
HashMap is the workhorse of Java's Collections Framework — flexible, fast, and feature-rich. But like every data structure, it has specific trade-offs you must understand to use it correctly.
Java HashMap — Interview Questions
These are the most frequently asked interview questions on Java HashMap in Java developer interviews — from junior to senior level. HashMap internals are among the most tested topics in Java collections interviews.
Practice Questions — Test Your Knowledge
Test your understanding of HashMap with these practice questions covering output prediction, design decisions, and internal behavior.
1. What will the following code output? HashMap<String,Integer> hm = new HashMap<>(); hm.put("A", 10); hm.put("B", 20); hm.put("A", 30); System.out.println(hm.size() + " " + hm.get("A"));
Easy2. What is the output? HashMap<String,String> hm = new HashMap<>(); hm.put(null, "zero"); hm.put(null, "updated"); System.out.println(hm.size() + " " + hm.get(null));
Easy3. You need to count character frequencies in a String using HashMap. Write the most concise correct approach using Java 8 methods.
Easy4. A HashMap has initialCapacity=16, loadFactor=0.75. After how many put() calls does the first rehash occur? What is the new capacity?
Medium5. What does the following output? Is there a bug? HashMap<String,String> hm = new HashMap<>(); hm.put("color", null); if (hm.get("color") == null) { System.out.println("color not found"); } else { System.out.println("color = " + hm.get("color")); }
Medium6. Two HashMap keys have hashCode() returning the same value but equals() returning false. What happens when both are put into a HashMap?
Medium7. Explain why the following code is NOT thread-safe even when using ConcurrentHashMap: if (!map.containsKey(key)) { map.put(key, value); }
Hard8. A custom class overrides hashCode() to always return 42 for all instances and equals() correctly. What is the performance impact on a HashMap with 1000 such keys?
HardConclusion — HashMap as the Foundation of Java Data Storage
HashMap is the backbone of key-value storage in Java. From simple configuration maps to complex in-memory caches and data aggregation pipelines, virtually every non-trivial Java application uses HashMap directly or through frameworks built on top of it. Its O(1) average-case performance, rich Java 8+ API, and null-key flexibility make it the default choice for the vast majority of Map use cases.
Understanding its internals — the power-of-two bucket array, hash spreading, separate chaining, treeification, load factor, and rehashing — is not just academic knowledge. It directly informs decisions about pre-sizing, key design (immutability and hashCode quality), and when to reach for LinkedHashMap, TreeMap, or ConcurrentHashMap instead.
Your next steps: study LinkedHashMap (insertion-order map and LRU cache patterns), TreeMap (Red-Black Tree sorted map and NavigableMap API), and ConcurrentHashMap (segment-level locking and atomic compound operations). Together these form the complete picture of Java's key-value storage ecosystem. ☕