HashMap Basics in Java: A Complete Guide

Introduction

The HashMap class in Java is one of the most widely used implementations of the Map interface in the Java Collections Framework. It stores data in key-value pairs, where each key is unique and maps to a single value. HashMap provides constant-time performance for basic operations like get() and put() under ideal conditions, making it highly efficient for lookups, caching, and data indexing. Understanding the fundamentals of HashMap—including its internal structure, usage, performance characteristics, and best practices—is essential for effective data management in Java applications.


1. Key Features of HashMap

  • Stores key-value pairs: Each key maps to exactly one value.
  • Allows one null key and multiple null values.
  • Does not maintain insertion order (use LinkedHashMap for ordered iteration).
  • Not synchronized: Not thread-safe by default (use ConcurrentHashMap for concurrent access).
  • Implements Map, Cloneable, and Serializable.
  • Backed by a hash table: Uses hashing to store and retrieve elements.

2. Creating and Initializing a HashMap

Basic Declaration

import java.util.HashMap;
import java.util.Map;
Map<String, Integer> phoneBook = new HashMap<>();

Best Practice: Program to the Map interface, not the implementation.

Initialization with Values (Java 9+)

Map<String, Integer> scores = Map.of(
"Alice", 95,
"Bob", 87,
"Charlie", 92
); // Immutable map
// For mutable HashMap:
Map<String, Integer> mutableScores = new HashMap<>(Map.of("Alice", 95, "Bob", 87));

Pre-Java 9 Initialization

Map<String, Integer> map = new HashMap<>();
map.put("Alice", 95);
map.put("Bob", 87);

3. Common Operations

A. Adding/Updating Entries

map.put("David", 88);        // Adds new entry
map.put("Alice", 98);        // Updates existing value

B. Retrieving Values

int aliceScore = map.get("Alice");     // 98
int unknown = map.get("Eve");          // null (key not present)

Note: get() returns null if the key is not found or if the value is null.

C. Checking Key/Value Existence

boolean hasAlice = map.containsKey("Alice");     // true
boolean hasScore99 = map.containsValue(99);      // false

D. Removing Entries

map.remove("Bob");           // Removes key "Bob"
map.remove("Alice", 95);     // Removes only if value is 95 (Java 8+)

E. Size and Emptiness

int size = map.size();       // Number of key-value pairs
boolean isEmpty = map.isEmpty();

F. Clearing All Entries

map.clear(); // Removes all mappings

4. Iterating Over a HashMap

A. Iterating Over Keys

for (String key : map.keySet()) {
System.out.println(key + " -> " + map.get(key));
}

B. Iterating Over Values

for (Integer value : map.values()) {
System.out.println(value);
}

C. Iterating Over Entries (Most Efficient)

for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}

D. Using forEach (Java 8+)

map.forEach((key, value) -> System.out.println(key + " -> " + value));

5. Internal Working: Hashing and Buckets

  • When a key-value pair is added, HashMap:
  1. Calls hashCode() on the key to compute a hash code.
  2. Applies a supplemental hash function to defend against poor hash codes.
  3. Uses the hash to determine the bucket index (array position).
  4. Stores the entry in a linked list (or tree if bucket size > 8) at that index.

Collision Handling:

  • Java 8+ converts linked lists to balanced trees when a bucket has more than 8 entries (improves worst-case performance from O(n) to O(log n)).

6. Performance Characteristics

OperationAverage Time ComplexityWorst Case
get(key)O(1)O(log n) (Java 8+, after tree conversion)
put(key, value)O(1)O(log n)
remove(key)O(1)O(log n)
IterationO(n)O(n)

Note: Performance assumes a good hash function and proper initial capacity.


7. Important Considerations

A. Key Requirements

  • hashCode() and equals() must be consistent:
  • If two keys are equal (equals() returns true), they must have the same hashCode().
  • Override both methods together in custom key classes.

B. Initial Capacity and Load Factor

  • Default initial capacity: 16
  • Default load factor: 0.75
  • When size > capacity × load factor, the map rehashes (resizes and reinserts all entries).

Optimization: Specify initial capacity if the approximate size is known:

Map<String, Integer> map = new HashMap<>(100); // Avoids rehashing

C. Thread Safety

  • HashMap is not thread-safe.
  • For concurrent access, use:
  • ConcurrentHashMap (preferred)
  • Collections.synchronizedMap(new HashMap<>())

8. HashMap vs. Other Map Implementations

FeatureHashMapLinkedHashMapTreeMap
OrderNo orderInsertion/access orderSorted (natural or custom)
Null KeysOneOneNot allowed (throws NullPointerException)
Time ComplexityO(1) avgO(1) avgO(log n)
Use CaseGeneral-purposePredictable iteration orderSorted keys

9. Best Practices

  • Use immutable objects as keys (e.g., String, Integer) to prevent accidental changes to hashCode().
  • Override hashCode() and equals() properly in custom key classes.
  • Specify initial capacity for large maps to avoid rehashing overhead.
  • Prefer ConcurrentHashMap over Collections.synchronizedMap() for better concurrency.
  • Avoid using null keys unless necessary—can lead to ambiguous get() results.
  • Use Map.of() or Map.copyOf() for small, immutable maps (Java 9+).

10. Common Use Cases

  • Caching: Store computed results by key.
  • Counting frequencies: Map<String, Integer> for word counts.
  • Configuration: Key-value settings.
  • Indexing: Map IDs to objects (e.g., Map<Integer, User>).
  • Lookup tables: Fast access by identifier.

Example: Word Frequency Counter

Map<String, Integer> wordCount = new HashMap<>();
String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
for (String word : words) {
wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
// Result: {apple=3, banana=2, cherry=1}

Tip: Use getOrDefault() (Java 8+) to simplify null checks.


Conclusion

HashMap is a cornerstone of efficient data storage and retrieval in Java, offering fast access to key-value pairs through hashing. Its simplicity, performance, and flexibility make it the go-to choice for most mapping needs. However, to use it effectively, developers must understand its behavior regarding hashing, null handling, thread safety, and performance trade-offs. By following best practices—such as using proper key types, setting appropriate initial capacity, and choosing the right concurrent alternative—programmers can leverage HashMap to build scalable, high-performance applications. Whether caching results, counting occurrences, or managing configurations, mastering HashMap is essential for any Java developer.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper