Introduction to HashMap
While lists ( ArrayList ) are perfect for storing sequences of items, many real-world problems require you to look up data based on a unique identifier. Think of a dictionary where you look up a word (the key) to find its definition (the value), or a phone book where you look up a name to find a number.
HashMap
is a part of the Java Collections Framework that implements the
Map
interface. It stores data in
key-value pairs
. It is designed to provide extremely fast lookups—on average, you can retrieve a value if you know its key in constant time, O(1), regardless of whether the map holds ten entries or ten million entries.
When to Use HashMap
Use a HashMap when you need to associate specific keys with values and perform frequent lookups, insertions, or deletions. It is ideal for caching data, counting frequencies, creating dictionaries, or mapping IDs to objects. Do
not
use it if you need to maintain the insertion order of elements (use
LinkedHashMap
) or if you need the keys to be automatically sorted (use
TreeMap
).
Prerequisites
Before working through this tutorial, you should understand:
- Basic variable types and object creation
-
How to use
Generics
(the
<Type, Type>syntax) -
Familiarity with the
ArrayList
and the
for-eachloop
What You Will Learn
- How to create, populate, and access a HashMap
-
The difference between
HashMap,Hashtable, andLinkedHashMap - How HashMap works internally (buckets, hashing, collisions)
-
The critical contract between
equals()andhashCode() - How to safely iterate over a map and modify it
- Common mistakes that cause memory leaks and data loss
How HashMap Works Internally
To use HashMap effectively—and to pass technical interviews—you need a basic understanding of its internals. A HashMap does not store entries in a single continuous block. Instead, it uses an array of buckets .
-
Hashing:
When you call
put(key, value), Java calls thehashCode()method on your key. This generates an integer. - Bucket Index: Java performs a mathematical operation on this hash code to calculate an index within the internal array (e.g., index 0 to 15 for an array of size 16).
- Storage: The key-value pair is stored in a "bucket" at that index.
- Collisions: If two different keys produce the same index (a hash collision), they are stored in the same bucket as a linked list (or a balanced tree if the list gets too long, as of Java 8).
-
Lookup:
When you call
get(key), Java recalculates the index, jumps directly to that bucket, and usesequals()to find the exact key in the list.
This is why lookups are O(1) on average: instead of searching the whole map, Java calculates exactly where the key should be and goes straight there.
HashMap Syntax
import java.util.HashMap;
import java.util.Map;
// Always program to the Map interface
Map<KeyType, ValueType> mapName = new HashMap<>();
// With an initial capacity (optimization)
Map<String, Integer> map = new HashMap<>(100);
Two Type Parameters
Unlike
ArrayList<T>
which takes one type,
HashMap
takes two: the type for the
Key
and the type for the
Value
. For example,
Map<String, Integer>
means the keys are Strings and the values are Integers.
Simple Example: Storing User Ages
import java.util.HashMap;
import java.util.Map;
public class HashMapBasics {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
// 1. Adding key-value pairs
ages.put("Alice", 28);
ages.put("Bob", 35);
ages.put("Charlie", 22);
// 2. Retrieving a value by key
System.out.println("Bob's age: " + ages.get("Bob"));
// 3. Updating a value (put replaces if key exists)
ages.put("Alice", 29);
System.out.println("Alice's updated age: " + ages.get("Alice"));
// 4. Checking if a key or value exists
System.out.println("Contains David? " + ages.containsKey("David"));
System.out.println("Contains age 22? " + ages.containsValue(22));
// 5. Safe retrieval with default value
System.out.println("David's age: " + ages.getOrDefault("David", -1));
// 6. Size of the map
System.out.println("Total entries: " + ages.size());
}
}
Important Methods Explained
-
put(key, value): Calculates the hash, finds the bucket, and inserts the pair. If the key already exists, it overwrites the old value and returns the old value. -
get(key): Calculates the hash, jumps to the bucket, usesequals()to find the key, and returns the value. Returnsnullif the key doesn't exist. -
getOrDefault(key, defaultValue): Identical toget(), but returns your specified default instead ofnullif the key is missing. This is heavily used in modern Java to preventNullPointerException. -
putIfAbsent(key, value): Only inserts the pair if the key does not already exist. Great for initializing caches safely.
Iterating Over a HashMap
Because a Map is not a
Collection
, you cannot iterate over it directly with a standard for-each loop. You must iterate over its
entry set
, its
key set
, or its
values
.
Map<String, Double> prices = new HashMap<>();
prices.put("Apple", 1.50);
prices.put("Banana", 0.75);
// 1. Iterate over Key-Value pairs (Most Common)
for (Map.Entry<String, Double> entry : prices.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: $" + entry.getValue());
}
// 2. Iterate over Keys only
for (String item : prices.keySet()) {
System.out.println("Item: " + item);
}
// 3. Iterate over Values only (Java 8+ forEach)
prices.values().forEach(price -> System.out.println("Price: $" + price));
Modifying a Map During Iteration
Just like with
ArrayList
, using
map.remove(key)
inside a standard for-each loop over the map will throw a
ConcurrentModificationException
. You must use an
Iterator
or safely use Java 8+ methods like
map.entrySet().removeIf(...)
.
Real-World Example: Word Frequency Counter
A classic use case for HashMap is counting the frequency of items. Let's build a program that counts how many times each word appears in a sentence.
import java.util.HashMap;
import java.util.Map;
public class WordCounter {
public static Map<String, Integer> countWords(String text) {
Map<String, Integer> frequencyMap = new HashMap<>();
if (text == null || text.trim().isEmpty()) {
return frequencyMap;
}
String[] words = text.toLowerCase().split("\\W+"); // Split by non-word characters
for (String word : words) {
// getOrDefault fetches current count, or 0 if word is new. Then we add 1.
int count = frequencyMap.getOrDefault(word, 0);
frequencyMap.put(word, count + 1);
}
return frequencyMap;
}
public static void main(String[] args) {
String sentence = "Java is great, and Java is powerful. I love Java!";
Map<String, Integer> counts = countWords(sentence);
// Printing results sorted by value (descending) using Java Streams
counts.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
}
}
This example perfectly showcases why HashMap is ideal: looking up whether a word already exists in the map takes O(1) time, making the overall algorithm O(n) where n is the number of words.
The Golden Rule: Custom Objects as Keys
Using
String
or
Integer
as keys is easy because Java has already written perfect
hashCode()
and
equals()
methods for them. However, if you use a custom
class
(like
Employee
or
User
) as a key,
you must override both methods
.
The Contract
-
If two objects are equal according to
equals(), they must return the same integer fromhashCode(). -
If you put an object in the map, then modify a field used in
hashCode(), you will "lose" the object in the map because its hash code changes.
import java.util.Objects;
public class EmployeeId {
private final String department;
private final int id;
public EmployeeId(String department, int id) {
this.department = department;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EmployeeId that = (EmployeeId) o;
return id == that.id && Objects.equals(department, that.department);
}
@Override
public int hashCode() {
return Objects.hash(department, id);
}
}
Crucial: Use 'final' Fields for Keys
Notice the
final
keyword on
department
and
id
in the class above. If a field used in
hashCode()
can be changed after the object is created, changing it will change the hash code. The map will look in the wrong bucket, and
get()
will return
null
even though the object is in the map. This is a notoriously difficult bug to track down.
Common Mistakes to Avoid
Mistake 1: Using get() Without Null Checks
Map<String, String> capitals = new HashMap<>();
capitals.put("France", "Paris");
// Throws NullPointerException if "Germany" is missing!
int length = capitals.get("Germany").length();
// Safe approach 1: getOrDefault
String capital = capitals.getOrDefault("Germany", "Unknown");
int length = capital.length();
// Safe approach 2: containsKey check
if (capitals.containsKey("Germany")) {
length = capitals.get("Germany").length();
}
Mistake 2: Mutable Keys
Never use a
List
,
ArrayList
, or a mutable custom class as a key. If the contents of the key change after insertion, the hash code changes, and the key becomes permanently lost in the map, causing a memory leak.
Mistake 3: Relying on HashMap Ordering
Do not assume
HashMap
will print or iterate in the order you inserted items. The order is dependent on the hash codes of the keys and the internal array size.
Best Practices
-
Program to the
Mapinterface: Always declare your variable asMap<K, V>, notHashMap<K, V>. This allows you to swap toLinkedHashMaporTreeMaplater without changing the rest of your code. -
Set Initial Capacity:
If you know your map will hold 10,000 items, initialize it with
new HashMap<>(10_000). This prevents expensive internal resizes and rehashing. -
Prefer
getOrDefault,putIfAbsent, andcomputeIfAbsent: These methods make map interactions safer and more concise by handling null checks internally. -
Avoid
HashMapin Multi-threaded Environments: If multiple threads will modify the map concurrently, useConcurrentHashMap. Do not useCollections.synchronizedMap()orHashtableunless absolutely necessary, asConcurrentHashMapis vastly superior in performance.
Performance Considerations
-
Time Complexity:
Average case for
getandputis O(1). Worst case (if all keys hash to the same bucket) is O(n), though Java 8's tree bins reduce this worst case to O(log n). -
Load Factor:
HashMap resizes when the number of entries exceeds
capacity * loadFactor. The default load factor is 0.75 (75% full). This balances memory usage against lookup speed. Increasing the load factor saves memory but slows down lookups due to more collisions. -
Memory Overhead:
HashMap has significant memory overhead compared to an array due to the bucket array, linked list nodes (or tree nodes), and the
Entryobjects. For primitive keys/values, consider specialized libraries like FastUtil or Trove to avoid boxing overhead.
Exercises
Exercise 1: Merge Two Maps
Write a method that takes two
Map<String, Integer>
objects and returns a new map containing all key-value pairs from both. If a key exists in both maps, the value from the second map should overwrite the value from the first map. (Hint: Look at the
putAll()
method).
Exercise 2: Character Frequency
Write a program that takes a
String
and uses a
HashMap<Character, Integer>
to count how many times each character (ignoring spaces) appears in the string. Print the results.
Solutions
Solution to Exercise 1: Merge Two Maps
import java.util.HashMap;
import java.util.Map;
public class MapMerger {
public static Map<String, Integer> mergeMaps(Map<String, Integer> map1, Map<String, Integer> map2) {
Map<String, Integer> merged = new HashMap<>(map1);
merged.putAll(map2); // Overwrites duplicates with map2's values
return merged;
}
public static void main(String[] args) {
Map<String, Integer> m1 = new HashMap<>();
m1.put("A", 1);
m1.put("B", 2);
Map<String, Integer> m2 = new HashMap<>();
m2.put("B", 99); // Will overwrite m1's "B"
m2.put("C", 3);
System.out.println(mergeMaps(m1, m2));
}
}
Solution to Exercise 2: Character Frequency
import java.util.HashMap;
import java.util.Map;
public class CharFrequency {
public static void main(String[] args) {
String text = "hello world";
Map<Character, Integer> counts = new HashMap<>();
for (char c : text.toCharArray()) {
if (c == ' ') continue; // Ignore spaces
// Using computeIfAbsent and merge for a modern approach
counts.merge(c, 1, Integer::sum);
}
System.out.println(counts);
}
}
Explanation:
The
merge(key, defaultValue, remappingFunction)
method is a powerful Java 8 addition. If the key is missing, it inserts the default value (1). If the key exists, it applies the remapping function (
Integer::sum
, which adds the old value and the new value together).
Summary
-
HashMapstores data in key-value pairs and provides O(1) average time complexity for lookups and insertions. -
It uses an array of buckets based on the key's
hashCode(). Collisions are handled via linked lists or tree bins. -
Always declare variables as
Map<K, V>, notHashMap<K, V>. -
If you use custom objects as keys, you
must
override
equals()andhashCode(), and the fields used should befinal. -
Use
getOrDefaultandputIfAbsentto write safer, cleaner code. -
HashMapdoes not maintain order. UseLinkedHashMapfor insertion order orTreeMapfor sorted order. -
It is not thread-safe; use
ConcurrentHashMapfor concurrent applications.
Frequently Asked Questions
HashMap
allows one null key and multiple null values, and is not synchronized, making it fast in single-threaded contexts.
Hashtable
is a legacy class that does not allow null keys or values, and is synchronized (thread-safe), making it much slower. In modern Java, if you need thread-safety, use
ConcurrentHashMap
instead of
Hashtable
.
No,
HashMap
makes no guarantees about the order of the map over time. The order depends entirely on the hash codes of the keys and the internal resizing of the map. If you need to preserve the order in which elements were inserted, you should use
LinkedHashMap
. If you need keys sorted in natural order (e.g., alphabetical), use
TreeMap
.
When
HashMap
looks up a key, it first calculates the hash code to find the bucket, then uses
equals()
to find the exact key in that bucket. If you don't override them, Java uses the default implementation from the
Object
class, which compares memory addresses. This means two distinct objects with identical data will be treated as entirely different keys.
This is called a hash collision. Both key-value pairs will be stored in the same bucket. Before Java 8, they were stored as a linked list, meaning lookup in that specific bucket degraded to O(n). In Java 8 and later, if a single bucket gets too many entries (usually more than 8), the linked list is transformed into a balanced red-black tree. This improves the worst-case lookup time in that bucket from O(n) to O(log n).
No. By definition, a Map maps
unique
keys to values. If you call
put()
with a key that already exists in the map, it will not create a duplicate entry. Instead, it will overwrite the existing value associated with that key, and the
put()
method will return the old value that was replaced.