HashMap
Imagine an employee directory where every employee ID should point to exactly one employee. You do not want to search through a list every time you need an employee. Instead, you want to provide the ID and quickly find the associated information.
This is the problem that HashMap solves. HashMap stores data as key-value pairs, allowing an application to associate one piece of information with another and efficiently retrieve a value using its key.
Core Idea: HashMap stores data as key-value pairs and uses hashing to provide efficient average-case insertion, lookup, and removal by key.
Why Does HashMap Exist?
Consider an application that needs to associate employee IDs with employee names.
101 - Bibhu 102 - Rahul 103 - Priya
A HashMap lets you express this relationship directly.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.put(103, "Priya");
System.out.println(
employees.get(101)
);
Instead of manually searching through a collection, you ask the Map for the value associated with key 101.
Simple analogy: Think of a HashMap like a dictionary. You look up a word using its key and receive the corresponding definition or value.
Map Is Not a Collection
A common beginner misconception is that Map extends Collection. It does not.
Collection
|
+-- List
|
+-- Set
|
+-- Queue
Map
|
+-- HashMap
Map represents a different abstraction: a mapping between keys and values.
Remember: List, Set, and Queue are Collection types. Map is a separate hierarchy for key-value mappings.
Creating a HashMap
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.put(103, "Priya");
System.out.println(employees);
}
}
Using the Map interface as the reference type is usually preferable when the application only depends on Map behavior.
Key-Value Pair
Every entry in a HashMap consists of a key and its associated value.
Key Value ---------------- 101 Bibhu 102 Rahul 103 Priya
The key is used to identify the mapping, while the value contains the associated data.
Adding Elements with put()
The put() method adds or updates a key-value mapping.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.put(103, "Priya");
Each key identifies one mapping in the Map.
Duplicate Keys
A Map cannot contain duplicate keys. If you insert the same key again, the existing value is replaced.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(101, "Bibhu Kumar");
System.out.println(
employees.get(101)
);
The second put() replaces the value associated with key 101.
Key Rule: HashMap allows only one mapping for a particular key. Calling put() with an existing key replaces its previous value.
Duplicate Values
Unlike keys, values do not have to be unique.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Developer");
employees.put(102, "Developer");
employees.put(103, "Tester");
System.out.println(employees);
Both 101 and 102 can have the same value, Developer.
Easy Rule: Keys must be unique within a Map. Values can be duplicated.
Getting a Value with get()
The get() method retrieves the value associated with a key.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
String name = employees.get(101);
System.out.println(name);
If the key exists, its associated value is returned.
What Happens When a Key Does Not Exist?
If the requested key is not present, get() returns null.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
System.out.println(
employees.get(999)
);
There is an important subtlety here: null can also be a legitimate mapped value. Therefore, get() returning null does not always prove that the key is absent.
containsKey()
When you specifically need to know whether a key exists, use containsKey().
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
if (employees.containsKey(101)) {
System.out.println("Employee exists");
}
Best Practice: Use containsKey() when key existence itself matters. Do not rely only on get() == null when null values are possible.
containsValue()
HashMap also provides containsValue() to check whether a value exists.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
if (employees.containsValue("Rahul")) {
System.out.println("Rahul found");
}
Value lookup is generally less efficient than key lookup because the Map is organized around keys.
Removing a Mapping
The remove() method removes the mapping associated with a key.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.remove(101);
System.out.println(employees);
The entire key-value mapping associated with 101 is removed.
Checking the Size
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
System.out.println(
employees.size()
);
The size represents the number of key-value mappings, not the total number of keys and values counted separately.
Checking Whether a Map Is Empty
Map<Integer, String> employees =
new HashMap<>();
if (employees.isEmpty()) {
System.out.println("No employees");
}
isEmpty() returns true when the Map contains no mappings.
Does HashMap Maintain Order?
No. HashMap does not guarantee insertion order.
Map<Integer, String> employees =
new HashMap<>();
employees.put(103, "Priya");
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
System.out.println(employees);
You must not build application logic that depends on the apparent order of HashMap iteration.
If order matters: Use LinkedHashMap for insertion order or TreeMap for sorted key order.
How HashMap Works Internally
HashMap is based on a hash table. When you insert a key-value pair, the key's hashCode() helps determine where the mapping belongs.
Key | v hashCode() | v Hash-based location | v Compare keys using equals() | v Store or find key-value entry
When you later call get(key), HashMap uses the key's hash information to locate the appropriate area and then compares candidate keys using equality.
This is why the relationship between equals() and hashCode() is fundamental to HashMap correctness.
Hash Collision
Different keys can produce the same hash code. This is called a hash collision.
Key A
|
+-- hashCode() = 500
\
+-- Same hash area
/
+-- hashCode() = 500
|
Key B
A collision does not mean the two keys are equal. HashMap can store different keys that happen to share the same hash code.
The implementation uses additional structure and equality comparison to distinguish entries within the same hash area.
HashMap and equals() / hashCode()
When custom objects are used as keys, the equals() and hashCode() contract becomes extremely important.
class Employee {
private int id;
public Employee(int id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Employee)) {
return false;
}
Employee other = (Employee) obj;
return id == other.id;
}
@Override
public int hashCode() {
return Integer.hashCode(id);
}
}
If two Employee objects have the same ID and are intended to represent the same logical key, equals() and hashCode() must agree.
Map<Employee, String> employees =
new HashMap<>();
employees.put(
new Employee(101),
"Bibhu"
);
System.out.println(
employees.get(
new Employee(101)
)
);
The second Employee object can retrieve the stored value because it is equal to the original key and produces the same hash code.
Golden Rule: If two key objects are equal according to equals(), they must return the same hashCode().
Mutable Keys: A Dangerous Mistake
Using mutable objects as HashMap keys can cause serious problems if fields involved in equals() or hashCode() change after insertion.
class Employee {
private int id;
public Employee(int id) {
this.id = id;
}
public void setId(int id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Employee)) {
return false;
}
Employee other = (Employee) obj;
return id == other.id;
}
@Override
public int hashCode() {
return Integer.hashCode(id);
}
}
If an Employee is inserted using ID 101 and its ID later changes to 999, the object's hash code changes. HashMap may then look in a different location when trying to find the key.
Best Practice: Prefer immutable objects as HashMap keys, especially when their equality and hash code depend on their state.
Null Keys and Null Values
HashMap permits a single null key and multiple null values.
Map<Integer, String> employees =
new HashMap<>();
employees.put(null, "Unknown");
employees.put(101, null);
employees.put(102, null);
System.out.println(employees);
There can be only one null key because keys must be unique, but multiple different keys can map to null.
Remember: HashMap allows one null key and multiple null values.
put() Return Value
An interesting detail is that put() returns the previous value associated with the key, or null if there was no previous mapping.
Map<Integer, String> employees =
new HashMap<>();
String oldValue =
employees.put(101, "Bibhu");
System.out.println(oldValue);
oldValue =
employees.put(101, "Rahul");
System.out.println(oldValue);
The first call returns null because key 101 did not previously exist. The second call returns Bibhu because that was the old value associated with the key.
putIfAbsent()
Sometimes you want to add a value only if the key is not already mapped to a value.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.putIfAbsent(
101,
"Rahul"
);
System.out.println(
employees.get(101)
);
The existing value is preserved because key 101 already has a mapping.
getOrDefault()
The getOrDefault() method is useful when you want a fallback value if a key is absent.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
String name =
employees.getOrDefault(
999,
"Unknown"
);
System.out.println(name);
This avoids writing a separate containsKey() check for many simple lookup scenarios.
replace()
replace() updates the value only when the key already exists.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.replace(
101,
"Bibhu Kumar"
);
System.out.println(
employees.get(101)
);
Unlike put(), replace() does not create a new mapping when the key is absent.
replaceAll()
You can transform all values using replaceAll().
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.replaceAll(
(id, name) -> name.toUpperCase()
);
System.out.println(employees);
The function receives each key and value and returns the new value.
remove() with Key and Value
HashMap also provides an overloaded remove() operation that removes a mapping only when both the key and value match.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
boolean removed =
employees.remove(
101,
"Rahul"
);
System.out.println(removed);
The mapping remains because the value associated with 101 is Bibhu, not Rahul.
Iterating Through HashMap
There are several ways to iterate through a Map. The most useful approach depends on whether you need keys, values, or both.
Iterating Through Keys
for (Integer id : employees.keySet()) {
System.out.println(id);
}
keySet() provides a view containing the Map's keys.
Iterating Through Values
for (String name : employees.values()) {
System.out.println(name);
}
values() provides a view containing the Map's values.
Iterating Through Entries
When you need both key and value, entrySet() is usually the clearest approach.
for (Map.Entry<Integer, String> entry
: employees.entrySet()) {
System.out.println(
entry.getKey()
+ " = "
+ entry.getValue()
);
}
Practical Tip: If you need both key and value, iterate over entrySet() rather than separately looking up each value from keySet().
forEach()
Modern Java also allows convenient iteration with Map.forEach().
employees.forEach(
(id, name) ->
System.out.println(
id + " = " + name
)
);
This is concise and works well for simple processing logic.
keySet(), values(), and entrySet()
| Method | Returns | Use When |
|---|---|---|
| keySet() | Set of keys | You need keys. |
| values() | Collection of values | You need values. |
| entrySet() | Set of key-value entries | You need both keys and values. |
Checking All Mappings
The entrySet() view is especially useful for displaying the complete mapping.
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.put(103, "Priya");
for (Map.Entry<Integer, String> entry
: employees.entrySet()) {
System.out.println(
"ID: " + entry.getKey()
+ ", Name: " + entry.getValue()
);
}
This style makes the relationship between each key and value explicit.
Counting Frequencies with HashMap
One of the most practical HashMap patterns is frequency counting.
Suppose you want to count how many times each word appears.
String[] words = {
"Java",
"Spring",
"Java",
"SQL",
"Java",
"Spring"
};
Map<String, Integer> frequency =
new HashMap<>();
for (String word : words) {
frequency.put(
word,
frequency.getOrDefault(word, 0) + 1
);
}
System.out.println(frequency);
The Map uses each word as a key and its occurrence count as the value. This pattern appears frequently in interviews and real-world data-processing code.
Grouping Data with HashMap
HashMap can also be used to group related information.
Map<String, List<String>> teams =
new HashMap<>();
teams.computeIfAbsent(
"Backend",
key -> new ArrayList<>()
).add("Bibhu");
teams.computeIfAbsent(
"Backend",
key -> new ArrayList<>()
).add("Rahul");
System.out.println(teams);
Here, the department name acts as the key and the associated List contains members belonging to that department.
computeIfAbsent()
The computeIfAbsent() method is especially useful when building grouped data structures.
Map<String, List<Integer>> scores =
new HashMap<>();
scores.computeIfAbsent(
"Java",
key -> new ArrayList<>()
).add(90);
scores.computeIfAbsent(
"Java",
key -> new ArrayList<>()
).add(95);
System.out.println(scores);
If the key is absent, the mapping function creates the initial value. If the key already exists, the existing value is returned.
merge()
The merge() method is useful when combining a new value with an existing mapping.
Map<String, Integer> frequency =
new HashMap<>();
frequency.merge(
"Java",
1,
Integer::sum
);
frequency.merge(
"Java",
1,
Integer::sum
);
System.out.println(frequency);
This is another concise way to build frequency counters.
HashMap Performance
| Operation | Average-Case Complexity | Purpose |
|---|---|---|
| put() | O(1) | Add or update a key-value mapping. |
| get() | O(1) | Retrieve a value by key. |
| containsKey() | O(1) | Check whether a key exists. |
| remove() | O(1) | Remove a mapping by key. |
| containsValue() | O(n) | Search through values. |
| Iteration | O(n) plus table-related overhead | Visit mappings, keys, or values. |
These are average-case expectations based on effective hashing. Hash collisions and implementation details can affect actual performance.
Initial Capacity and Load Factor
HashMap uses a hash-table structure whose capacity can grow as mappings are added. Two important concepts are initial capacity and load factor.
Map<Integer, String> employees =
new HashMap<>(100, 0.75f);
The initial capacity provides a starting size for the hash table, while the load factor helps determine when resizing should occur.
Choosing capacity intelligently can reduce unnecessary resizing when the approximate number of entries is known in advance.
Important: HashMap capacity is not the same thing as map size. size() counts actual key-value mappings; capacity refers to the underlying hash-table structure.
HashMap vs Hashtable
HashMap and Hashtable are both hash-based Map implementations, but they differ in important ways.
| Feature | HashMap | Hashtable |
|---|---|---|
| Null key | One allowed | Not allowed |
| Null values | Allowed | Not allowed |
| Synchronization | Not inherently synchronized | Legacy synchronized implementation |
| Modern default | Common choice | Generally avoided for new code |
For modern concurrent applications, use collections designed for concurrency rather than relying on legacy Hashtable behavior.
HashMap vs LinkedHashMap vs TreeMap
These three Map implementations are worth memorizing because they represent three different ordering strategies.
| Feature | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| Key-value mapping | Yes | Yes | Yes |
| Key uniqueness | Yes | Yes | Yes |
| Insertion order | Not guaranteed | Maintained | Not its ordering model |
| Sorted keys | No | No | Yes |
| Average/basic lookup | O(1) | O(1) | O(log n) |
| Typical use | Fast key-based lookup | Lookup plus insertion-order iteration | Sorted keys and navigation |
HashMap with Custom Keys
Custom objects are often used as keys in real applications. For example, an application might use an Employee object as the key for employee-specific metadata.
Map<Employee, String> departments =
new HashMap<>();
departments.put(
new Employee(101),
"Backend"
);
For this to work reliably, the Employee class must obey the equals() and hashCode() contract.
Production Tip: Prefer stable, immutable identifiers such as IDs, UUIDs, or immutable value objects as Map keys whenever practical.
Updating a Map Safely
A common pattern is to update a value only if a key exists.
if (employees.containsKey(101)) {
employees.put(
101,
"Bibhu Kumar"
);
}
For simple replacement, replace() can express the intention more clearly.
employees.replace(
101,
"Bibhu Kumar"
);
Clearing a HashMap
Map<Integer, String> employees =
new HashMap<>();
employees.put(101, "Bibhu");
employees.put(102, "Rahul");
employees.clear();
System.out.println(
employees.isEmpty()
);
clear() removes all mappings from the Map.
Common Beginner Mistakes
- Assuming duplicate keys are stored: A new value replaces the previous value for the same key.
- Assuming insertion order: HashMap does not guarantee iteration order.
- Using get() == null to prove absence: A key can exist and still map to null. Use containsKey() when necessary.
- Ignoring equals() and hashCode(): Custom key classes require a correct equality contract.
- Mutating keys: Changing hash-relevant state after insertion can make a mapping difficult to retrieve.
- Using keySet() and calling get() repeatedly when entrySet() is enough: Iterate over entries when both key and value are required.
- Assuming HashMap is thread-safe: It is not inherently safe for concurrent structural modification.
- Confusing Map size with table capacity: size() reports mappings, not internal bucket capacity.
Best Practices
- Use HashMap when efficient average-case key-based lookup is the main requirement.
- Declare the reference using Map when implementation-specific behavior is unnecessary.
- Use immutable or stable objects as keys whenever practical.
- Implement equals() and hashCode() consistently for custom key types.
- Use containsKey() when distinguishing an absent key from a key mapped to null matters.
- Use entrySet() when both keys and values are needed during iteration.
- Use LinkedHashMap when insertion order matters.
- Use TreeMap when sorted key order or navigation operations are required.
- Use appropriate concurrent Map implementations when multiple threads access shared mappings.
Interview Insights
Question: What is HashMap?
Answer: HashMap is a hash-table-based Map implementation that stores key-value pairs and provides efficient average-case insertion, lookup, and removal by key.
Question: Can HashMap contain duplicate keys?
Answer: No. A key can appear only once. Inserting the same key again replaces its previous value.
Question: Can HashMap contain duplicate values?
Answer: Yes. Multiple different keys can map to the same value.
Question: How does HashMap find a value?
Answer: HashMap uses the key's hash information to locate a hash-table area and then uses equality comparison to identify the matching key.
Question: What is the average time complexity of get() and put()?
Answer: They are typically O(1) on average with effective hash distribution, although actual performance depends on collisions and implementation details.
Question: Can HashMap have a null key?
Answer: Yes. HashMap permits one null key and multiple null values.
Question: Why are equals() and hashCode() important for HashMap keys?
Answer: HashMap uses hashCode() to help locate candidate entries and equals() to determine key equality. Equal keys must produce the same hash code.
Question: Why is entrySet() commonly preferred when both key and value are needed?
Answer: entrySet() directly exposes each key-value mapping, making the iteration clear and avoiding an unnecessary separate lookup for each value.
Quick Revision
| Concept | Key Point |
|---|---|
| HashMap | Hash-based Map implementation for key-value mappings. |
| Keys | Must be unique. |
| Values | Can be duplicated. |
| Order | No guaranteed insertion order. |
| put() | Adds or replaces a mapping and returns the previous value. |
| get() | Retrieves a value using its key. |
| containsKey() | Checks whether a key exists. |
| remove() | Removes a mapping by key. |
| Null key | One null key is allowed. |
| Null values | Multiple null values are allowed. |
| Average get/put/remove | Typically O(1). |
| equals()/hashCode() | Critical for correct behavior with custom keys. |
| LinkedHashMap | Use when insertion order matters. |
| TreeMap | Use when sorted keys are required. |
HashMap is one of the most important Java collections to master because key-value lookup appears everywhere: caches, configuration data, indexes, frequency counters, grouping logic, lookup tables, and application-level mappings. The real skill is not memorizing get() and put(), but understanding how hashing, equality, key uniqueness, mutability, and ordering affect the behavior of the Map. In the next chapter, we will explore LinkedHashMap, which adds predictable insertion-order iteration to hash-based Map behavior.
