Table of Contents
- Introduction to HashMap
- HashMap Internal Structure
- Basic Operations
- Key Requirements
- Advanced Operations
- Performance Characteristics
- Thread Safety and ConcurrentHashMap
- Real-World Examples
- Best Practices
Introduction to HashMap
HashMap is a hash table-based implementation of the Map interface that stores key-value pairs. It provides constant-time performance for basic operations (get and put) assuming the hash function disperses elements properly.
Basic HashMap Creation
import java.util.*;
public class HashMapBasic {
public static void main(String[] args) {
// Creating HashMaps
HashMap<String, Integer> map1 = new HashMap<>(); // Default capacity 16, load factor 0.75
HashMap<String, String> map2 = new HashMap<>(20); // Initial capacity 20
HashMap<String, Double> map3 = new HashMap<>(20, 0.8f); // Capacity 20, load factor 0.8
// Creating from existing map
Map<String, Integer> existingMap = new HashMap<>();
existingMap.put("A", 1);
existingMap.put("B", 2);
HashMap<String, Integer> map4 = new HashMap<>(existingMap);
System.out.println("Map4: " + map4);
}
}
HashMap Internal Structure
How HashMap Works Internally
// Simplified view of HashMap internals
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// Array of buckets
transient Node<K,V>[] table;
// Node class representing key-value pairs
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // For handling collisions
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
// Important parameters
int size; // Number of key-value mappings
int modCount; // Structural modifications
int threshold; // Next size value at which to resize
final float loadFactor; // Load factor (default 0.75)
}
Hash Collision Handling
public class HashMapInternalDemo {
public static void explainInternalWorking() {
// HashMap uses separate chaining for collision resolution
HashMap<String, Integer> map = new HashMap<>();
// When we put a key-value pair:
// 1. Calculate hash code of key
// 2. Use hash to find bucket index: index = hash & (n-1)
// 3. If bucket is empty, create new node
// 4. If bucket has nodes, traverse linked list/ tree
// 5. If key exists, update value; else add new node
map.put("John", 25);
map.put("Jane", 30);
map.put("Doe", 35);
System.out.println("HashMap structure demonstration:");
System.out.println("Load factor: 0.75 (when 75% full, HashMap doubles in size)");
System.out.println("Initial capacity: 16 buckets");
}
public static void demonstrateCollisions() {
HashMap<String, String> map = new HashMap<>(4); // Small capacity to force collisions
// These might end up in same bucket due to small capacity
map.put("A", "Apple");
map.put("E", "Elephant"); // Might collide with A
map.put("I", "Ice cream"); // Might collide with A and E
System.out.println("\nCollision demonstration:");
System.out.println("With small capacity, multiple keys may hash to same bucket");
System.out.println("HashMap handles this using linked lists (or trees for many collisions)");
}
}
Basic Operations
CRUD Operations
import java.util.*;
public class HashMapCRUD {
public static void main(String[] args) {
HashMap<String, Integer> studentGrades = new HashMap<>();
// CREATE - Adding key-value pairs
studentGrades.put("Alice", 85);
studentGrades.put("Bob", 92);
studentGrades.put("Charlie", 78);
studentGrades.put("Diana", 95);
System.out.println("Initial Map: " + studentGrades);
// READ - Retrieving values
Integer aliceGrade = studentGrades.get("Alice");
Integer unknownGrade = studentGrades.get("Eve"); // Returns null
System.out.println("Alice's grade: " + aliceGrade);
System.out.println("Eve's grade: " + unknownGrade);
// Check if key exists
boolean hasBob = studentGrades.containsKey("Bob");
boolean hasGrade95 = studentGrades.containsValue(95);
System.out.println("Contains Bob: " + hasBob);
System.out.println("Contains grade 95: " + hasGrade95);
// UPDATE - Modifying values
studentGrades.put("Charlie", 82); // Update existing key
studentGrades.replace("Bob", 94); // Only updates if key exists
// Put if absent
studentGrades.putIfAbsent("Eve", 88); // Adds only if key doesn't exist
studentGrades.putIfAbsent("Alice", 90); // Won't update Alice
System.out.println("After updates: " + studentGrades);
// DELETE - Removing entries
studentGrades.remove("Diana"); // Remove by key
studentGrades.remove("Bob", 92); // Remove only if key-value matches (Bob still has 94)
System.out.println("After deletions: " + studentGrades);
// Clear all
studentGrades.clear();
System.out.println("After clear: " + studentGrades);
}
}
Bulk Operations
public class HashMapBulkOperations {
public static void main(String[] args) {
HashMap<String, String> countryCapitals = new HashMap<>();
// Put all from another map
Map<String, String> europe = new HashMap<>();
europe.put("Germany", "Berlin");
europe.put("France", "Paris");
europe.put("Italy", "Rome");
countryCapitals.putAll(europe);
System.out.println("After putAll: " + countryCapitals);
// Size and emptiness checks
System.out.println("Size: " + countryCapitals.size());
System.out.println("Is empty: " + countryCapitals.isEmpty());
// Key, value, and entry sets
Set<String> countries = countryCapitals.keySet();
Collection<String> capitals = countryCapitals.values();
Set<Map.Entry<String, String>> entries = countryCapitals.entrySet();
System.out.println("Countries: " + countries);
System.out.println("Capitals: " + capitals);
System.out.println("Entries: " + entries);
}
}
Key Requirements
Importance of equals() and hashCode()
import java.util.*;
class Student {
private String id;
private String name;
private int age;
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// BAD: Without equals and hashCode - HashMap won't work correctly!
// GOOD: Proper implementation
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "Student{id='" + id + "', name='" + name + "', age=" + age + "}";
}
}
public class KeyRequirementsDemo {
public static void main(String[] args) {
HashMap<Student, String> courseRegistrations = new HashMap<>();
Student student1 = new Student("S001", "John Doe", 20);
Student student2 = new Student("S001", "John Doe", 20); // Same ID, different object
courseRegistrations.put(student1, "Computer Science");
// With proper equals/hashCode, this will find the existing student
String course = courseRegistrations.get(student2);
System.out.println("Course for student2: " + course); // Should be "Computer Science"
// Without proper equals/hashCode, this would return null!
}
}
Immutable Keys
import java.util.*;
// Good practice: Use immutable objects as keys
final class ImmutableStudent {
private final String id;
private final String name;
public ImmutableStudent(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutableStudent that = (ImmutableStudent) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "ImmutableStudent{id='" + id + "', name='" + name + "'}";
}
}
public class ImmutableKeysDemo {
public static void main(String[] args) {
HashMap<ImmutableStudent, Integer> grades = new HashMap<>();
ImmutableStudent student = new ImmutableStudent("S001", "Alice");
grades.put(student, 85);
// Since student is immutable, we can safely use it as a key
// If we could modify the student, the HashMap could become corrupted
System.out.println("Grade: " + grades.get(student));
// String, Integer, etc. are good keys because they're immutable
HashMap<String, Integer> stringKeyMap = new HashMap<>();
stringKeyMap.put("key1", 100);
// This is safe because Strings are immutable
String key = "key1";
stringKeyMap.put(key, 200);
}
}
Advanced Operations
Iteration Techniques
import java.util.*;
public class HashMapIteration {
public static void main(String[] args) {
HashMap<String, Integer> population = new HashMap<>();
population.put("China", 1439323776);
population.put("India", 1380004385);
population.put("USA", 331002651);
population.put("Indonesia", 273523615);
System.out.println("=== Different Iteration Methods ===");
// 1. Iterate over keys
System.out.println("\n1. Iterating over keys:");
for (String country : population.keySet()) {
System.out.println("Country: " + country);
}
// 2. Iterate over values
System.out.println("\n2. Iterating over values:");
for (Integer pop : population.values()) {
System.out.println("Population: " + pop);
}
// 3. Iterate over entries (most efficient)
System.out.println("\n3. Iterating over entries:");
for (Map.Entry<String, Integer> entry : population.entrySet()) {
System.out.println("Country: " + entry.getKey() + ", Population: " + entry.getValue());
}
// 4. Using forEach with lambda (Java 8+)
System.out.println("\n4. Using forEach with lambda:");
population.forEach((country, pop) ->
System.out.println("Country: " + country + ", Population: " + pop));
// 5. Using iterator
System.out.println("\n5. Using iterator:");
Iterator<Map.Entry<String, Integer>> iterator = population.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
if (entry.getValue() > 1000000000) {
System.out.println("Large population: " + entry.getKey());
}
}
}
}
Java 8+ Features
import java.util.*;
public class HashMapJava8Features {
public static void main(String[] args) {
HashMap<String, Integer> scores = new HashMap<>();
scores.put("Alice", 85);
scores.put("Bob", 92);
scores.put("Charlie", 78);
System.out.println("Original: " + scores);
// compute() - compute a new value for a key
scores.compute("Alice", (key, value) -> value + 5); // Add 5 to Alice's score
System.out.println("After compute on Alice: " + scores);
// computeIfAbsent() - compute if key doesn't exist
scores.computeIfAbsent("Diana", key -> 90); // Diana didn't exist, so add with 90
scores.computeIfAbsent("Alice", key -> 100); // Alice exists, so no change
System.out.println("After computeIfAbsent: " + scores);
// computeIfPresent() - compute only if key exists
scores.computeIfPresent("Bob", (key, value) -> value - 2); // Bob exists, subtract 2
scores.computeIfPresent("Eve", (key, value) -> 95); // Eve doesn't exist, no change
System.out.println("After computeIfPresent: " + scores);
// merge() - merge values
HashMap<String, Integer> newScores = new HashMap<>();
newScores.put("Alice", 10);
newScores.put("Frank", 88);
newScores.forEach((key, value) ->
scores.merge(key, value, (oldVal, newVal) -> oldVal + newVal));
System.out.println("After merge: " + scores);
// getOrDefault() - get value or default if not present
int eveScore = scores.getOrDefault("Eve", 0);
System.out.println("Eve's score (default 0): " + eveScore);
// putIfAbsent() - put only if key doesn't exist
scores.putIfAbsent("Charlie", 95); // Charlie exists, no change
scores.putIfAbsent("Eve", 85); // Eve doesn't exist, so added
System.out.println("After putIfAbsent: " + scores);
}
}
Sorting HashMap
import java.util.*;
import java.util.stream.*;
public class HashMapSorting {
public static void main(String[] args) {
HashMap<String, Integer> studentScores = new HashMap<>();
studentScores.put("Alice", 85);
studentScores.put("Bob", 92);
studentScores.put("Charlie", 78);
studentScores.put("Diana", 95);
studentScores.put("Eve", 88);
System.out.println("Original: " + studentScores);
// Sort by keys
System.out.println("\n=== Sort by Key ===");
// Using TreeMap (automatically sorts by keys)
TreeMap<String, Integer> sortedByKey = new TreeMap<>(studentScores);
System.out.println("Sorted by key (TreeMap): " + sortedByKey);
// Using streams
Map<String, Integer> sortedByKeyStream = studentScores.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
System.out.println("Sorted by key (stream): " + sortedByKeyStream);
// Sort by values
System.out.println("\n=== Sort by Value ===");
Map<String, Integer> sortedByValue = studentScores.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
System.out.println("Sorted by value (ascending): " + sortedByValue);
// Sort by value descending
Map<String, Integer> sortedByValueDesc = studentScores.entrySet()
.stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
System.out.println("Sorted by value (descending): " + sortedByValueDesc);
}
}
Performance Characteristics
Time Complexity Analysis
import java.util.*;
public class HashMapPerformance {
private static final int SIZE = 100000;
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
// Test put performance
long startTime = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
map.put(i, "Value" + i);
}
long putTime = System.nanoTime() - startTime;
// Test get performance
startTime = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
map.get(i);
}
long getTime = System.nanoTime() - startTime;
// Test containsKey performance
startTime = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
map.containsKey(i);
}
long containsTime = System.nanoTime() - startTime;
System.out.println("Performance for " + SIZE + " elements:");
System.out.printf("Put operations: %10d ns (O(1) amortized)%n", putTime);
System.out.printf("Get operations: %10d ns (O(1) average)%n", getTime);
System.out.printf("Contains operations: %7d ns (O(1) average)%n", containsTime);
// Demonstrate worst-case scenario (all keys in same bucket)
demonstrateWorstCase();
}
public static void demonstrateWorstCase() {
// Bad hashCode that always returns same value
class BadKey {
int id;
BadKey(int id) { this.id = id; }
@Override
public int hashCode() { return 1; } // Always same hash!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BadKey badKey = (BadKey) o;
return id == badKey.id;
}
}
HashMap<BadKey, String> badMap = new HashMap<>();
long startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
badMap.put(new BadKey(i), "Value" + i);
}
long badPutTime = System.nanoTime() - startTime;
System.out.println("\nWorst-case performance (poor hashCode):");
System.out.printf("Put operations: %10d ns (O(n) due to collisions)%n", badPutTime);
}
}
Load Factor and Resizing
public class HashMapResizing {
public static void demonstrateResizing() {
// HashMap resizes when size > capacity * loadFactor
// Default: capacity=16, loadFactor=0.75, resize at 12 elements
HashMap<String, Integer> map = new HashMap<>(4, 0.75f); // Small for demonstration
System.out.println("Demonstrating HashMap resizing:");
for (int i = 1; i <= 10; i++) {
map.put("Key" + i, i);
System.out.printf("Added Key%d, size=%d, should resize at %d%n",
i, map.size(), (int)(4 * 0.75));
}
// After resize, capacity doubles (to 8), threshold becomes 6
System.out.println("After resizing, new threshold: " + (int)(8 * 0.75));
}
public static void optimizePerformance() {
// If you know the expected size, pre-size the HashMap
int expectedSize = 1000;
float loadFactor = 0.75f;
// Calculate initial capacity to avoid resizing
int initialCapacity = (int) Math.ceil(expectedSize / loadFactor);
HashMap<String, String> optimizedMap = new HashMap<>(initialCapacity, loadFactor);
System.out.println("\nOptimized HashMap:");
System.out.println("Expected size: " + expectedSize);
System.out.println("Initial capacity: " + initialCapacity);
System.out.println("This will avoid resizing until " + expectedSize + " elements");
}
public static void main(String[] args) {
demonstrateResizing();
optimizePerformance();
}
}
Thread Safety and ConcurrentHashMap
HashMap Thread Safety Issues
import java.util.*;
import java.util.concurrent.*;
public class HashMapThreadSafety {
public static void demonstrateRaceCondition() throws InterruptedException {
HashMap<Integer, Integer> unsafeMap = new HashMap<>();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
unsafeMap.put(i, i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// The size might not be 1000 due to race conditions
System.out.println("Unsafe HashMap size: " + unsafeMap.size() + " (should be 1000)");
}
public static void usingSynchronizedMap() throws InterruptedException {
Map<Integer, Integer> safeMap = Collections.synchronizedMap(new HashMap<>());
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
safeMap.put(i, i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Synchronized HashMap size: " + safeMap.size() + " (should be 1000)");
}
public static void usingConcurrentHashMap() throws InterruptedException {
ConcurrentHashMap<Integer, Integer> concurrentMap = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
concurrentMap.put(i, i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("ConcurrentHashMap size: " + concurrentMap.size() + " (should be 1000)");
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Thread Safety Demonstration ===");
// This might show issues (depending on timing)
// demonstrateRaceCondition();
usingSynchronizedMap();
usingConcurrentHashMap();
System.out.println("\nNote: HashMap is not thread-safe!");
System.out.println("Use ConcurrentHashMap for concurrent access");
System.out.println("Or Collections.synchronizedMap() for synchronized access");
}
}
Real-World Examples
Configuration Management
import java.util.*;
public class ConfigurationManager {
private final HashMap<String, Object> config = new HashMap<>();
public ConfigurationManager() {
// Load default configuration
loadDefaultConfig();
}
private void loadDefaultConfig() {
config.put("app.name", "MyApplication");
config.put("app.version", "1.0.0");
config.put("database.url", "jdbc:mysql://localhost:3306/mydb");
config.put("database.username", "admin");
config.put("cache.enabled", true);
config.put("cache.size", 1000);
config.put("log.level", "INFO");
}
public void setConfig(String key, Object value) {
config.put(key, value);
}
public Object getConfig(String key) {
return config.get(key);
}
public String getString(String key) {
Object value = config.get(key);
return value != null ? value.toString() : null;
}
public int getInt(String key, int defaultValue) {
Object value = config.get(key);
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return defaultValue;
}
public boolean getBoolean(String key, boolean defaultValue) {
Object value = config.get(key);
if (value instanceof Boolean) {
return (Boolean) value;
} else if (value instanceof String) {
return Boolean.parseBoolean((String) value);
}
return defaultValue;
}
public void loadFromMap(Map<String, Object> newConfig) {
config.putAll(newConfig);
}
public void printConfig() {
System.out.println("=== Application Configuration ===");
config.forEach((key, value) ->
System.out.printf("%-20s: %s%n", key, value));
}
public static void main(String[] args) {
ConfigurationManager configManager = new ConfigurationManager();
// Override some settings
configManager.setConfig("log.level", "DEBUG");
configManager.setConfig("cache.size", 2000);
// Load multiple settings at once
HashMap<String, Object> customSettings = new HashMap<>();
customSettings.put("database.timeout", 30);
customSettings.put("feature.experimental", true);
configManager.loadFromMap(customSettings);
// Access configuration
System.out.println("App Name: " + configManager.getString("app.name"));
System.out.println("Cache Size: " + configManager.getInt("cache.size", 1000));
System.out.println("Experimental Feature: " + configManager.getBoolean("feature.experimental", false));
configManager.printConfig();
}
}
Cache Implementation
import java.util.*;
public class SimpleCache<K, V> {
private final HashMap<K, CacheEntry<V>> cache;
private final int maxSize;
private final long defaultTtl; // Time to live in milliseconds
private static class CacheEntry<V> {
private final V value;
private final long expiryTime;
CacheEntry(V value, long ttl) {
this.value = value;
this.expiryTime = System.currentTimeMillis() + ttl;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
}
public SimpleCache(int maxSize, long defaultTtl) {
this.cache = new HashMap<>();
this.maxSize = maxSize;
this.defaultTtl = defaultTtl;
}
public void put(K key, V value) {
put(key, value, defaultTtl);
}
public void put(K key, V value, long ttl) {
// Remove expired entries before adding
cleanExpiredEntries();
// If cache is full, remove some entries (simple strategy: remove first few)
if (cache.size() >= maxSize) {
Iterator<K> iterator = cache.keySet().iterator();
for (int i = 0; i < maxSize / 10 && iterator.hasNext(); i++) {
iterator.next();
iterator.remove();
}
}
cache.put(key, new CacheEntry<>(value, ttl));
}
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry == null) {
return null; // Not in cache
}
if (entry.isExpired()) {
cache.remove(key); // Remove expired entry
return null;
}
return entry.value;
}
public boolean containsKey(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry != null && entry.isExpired()) {
cache.remove(key);
return false;
}
return entry != null;
}
public void remove(K key) {
cache.remove(key);
}
public void clear() {
cache.clear();
}
public int size() {
cleanExpiredEntries();
return cache.size();
}
private void cleanExpiredEntries() {
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
public void printCache() {
System.out.println("=== Cache Contents ===");
cache.forEach((key, entry) -> {
String status = entry.isExpired() ? "EXPIRED" : "VALID";
System.out.printf("Key: %s, Value: %s, Status: %s%n",
key, entry.value, status);
});
}
public static void main(String[] args) throws InterruptedException {
SimpleCache<String, String> cache = new SimpleCache<>(5, 2000); // 2 second TTL
cache.put("key1", "value1");
cache.put("key2", "value2", 5000); // 5 second TTL
cache.put("key3", "value3");
System.out.println("Initial cache:");
cache.printCache();
System.out.println("\nGetting key1: " + cache.get("key1"));
System.out.println("Contains key2: " + cache.containsKey("key2"));
// Wait for some entries to expire
Thread.sleep(3000);
System.out.println("\nAfter 3 seconds:");
cache.printCache();
System.out.println("\nGetting key1 (should be expired): " + cache.get("key1"));
System.out.println("Getting key2 (should still be valid): " + cache.get("key2"));
}
}
Student Grade Management System
import java.util.*;
public class StudentGradeManager {
private final HashMap<String, HashMap<String, Double>> studentGrades;
public StudentGradeManager() {
this.studentGrades = new HashMap<>();
}
public void addStudent(String studentId, String name) {
studentGrades.put(studentId, new HashMap<>());
}
public void addGrade(String studentId, String course, double grade) {
HashMap<String, Double> grades = studentGrades.get(studentId);
if (grades != null) {
grades.put(course, grade);
} else {
System.out.println("Student not found: " + studentId);
}
}
public Double getGrade(String studentId, String course) {
HashMap<String, Double> grades = studentGrades.get(studentId);
return grades != null ? grades.get(course) : null;
}
public double getAverageGrade(String studentId) {
HashMap<String, Double> grades = studentGrades.get(studentId);
if (grades == null || grades.isEmpty()) {
return 0.0;
}
double sum = 0.0;
for (double grade : grades.values()) {
sum += grade;
}
return sum / grades.size();
}
public HashMap<String, Double> getStudentGrades(String studentId) {
return new HashMap<>(studentGrades.getOrDefault(studentId, new HashMap<>()));
}
public Set<String> getStudentsInCourse(String course) {
Set<String> students = new HashSet<>();
for (Map.Entry<String, HashMap<String, Double>> entry : studentGrades.entrySet()) {
if (entry.getValue().containsKey(course)) {
students.add(entry.getKey());
}
}
return students;
}
public double getCourseAverage(String course) {
double sum = 0.0;
int count = 0;
for (HashMap<String, Double> grades : studentGrades.values()) {
Double grade = grades.get(course);
if (grade != null) {
sum += grade;
count++;
}
}
return count > 0 ? sum / count : 0.0;
}
public void printAllStudents() {
System.out.println("=== All Students and Grades ===");
studentGrades.forEach((studentId, grades) -> {
double average = getAverageGrade(studentId);
System.out.printf("Student: %s, Average: %.2f%n", studentId, average);
grades.forEach((course, grade) ->
System.out.printf(" %s: %.2f%n", course, grade));
});
}
public static void main(String[] args) {
StudentGradeManager manager = new StudentGradeManager();
// Add students
manager.addStudent("S001", "Alice");
manager.addStudent("S002", "Bob");
manager.addStudent("S003", "Charlie");
// Add grades
manager.addGrade("S001", "Math", 85.5);
manager.addGrade("S001", "Science", 92.0);
manager.addGrade("S001", "History", 78.5);
manager.addGrade("S002", "Math", 90.0);
manager.addGrade("S002", "Science", 88.5);
manager.addGrade("S003", "Math", 75.0);
manager.addGrade("S003", "History", 82.0);
// Query data
System.out.println("Alice's Math grade: " + manager.getGrade("S001", "Math"));
System.out.println("Alice's average: " + manager.getAverageGrade("S001"));
System.out.println("Math course average: " + manager.getCourseAverage("Math"));
System.out.println("\nStudents in Science: " + manager.getStudentsInCourse("Science"));
manager.printAllStudents();
}
}
Best Practices
HashMap Best Practices
import java.util.*;
public class HashMapBestPractices {
// 1. Use immutable keys
public static void useImmutableKeys() {
// Good: String, Integer, etc.
HashMap<String, Integer> goodMap = new HashMap<>();
goodMap.put("key1", 1);
// Bad: Mutable keys can cause issues
class MutableKey {
String value;
MutableKey(String value) { this.value = value; }
// No proper equals/hashCode implementation
}
HashMap<MutableKey, String> badMap = new HashMap<>();
MutableKey key = new MutableKey("original");
badMap.put(key, "value");
key.value = "modified"; // Now the key's hash changed!
// The value becomes unreachable or causes incorrect behavior
}
// 2. Implement proper equals() and hashCode()
public static void properEqualsHashCode() {
class ProperKey {
final String id;
final String name;
ProperKey(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProperKey properKey = (ProperKey) o;
return Objects.equals(id, properKey.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
HashMap<ProperKey, String> map = new HashMap<>();
ProperKey key1 = new ProperKey("1", "Alice");
ProperKey key2 = new ProperKey("1", "Alice"); // Different object, same id
map.put(key1, "Value1");
String value = map.get(key2); // This will work correctly
System.out.println("Value for key2: " + value); // Should be "Value1"
}
// 3. Pre-size HashMap when size is known
public static void preSizeHashMap() {
int expectedSize = 1000;
float loadFactor = 0.75f;
// Calculate initial capacity
int initialCapacity = (int) Math.ceil(expectedSize / loadFactor);
HashMap<String, String> optimized = new HashMap<>(initialCapacity, loadFactor);
System.out.println("Pre-sized HashMap for " + expectedSize + " elements");
System.out.println("Initial capacity: " + initialCapacity);
}
// 4. Use entrySet for iteration when both key and value are needed
public static void efficientIteration() {
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// Efficient: entrySet iteration
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Less efficient: keySet with get
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key)); // Extra hash lookup
}
}
// 5. Use getOrDefault for safe value retrieval
public static void safeValueRetrieval() {
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 100);
// Safe way to get values
int valueA = map.getOrDefault("A", 0);
int valueB = map.getOrDefault("B", 0); // Returns 0 instead of null
System.out.println("Value A: " + valueA);
System.out.println("Value B: " + valueB);
}
// 6. Consider thread safety
public static void threadSafetyConsideration() {
// Single-threaded: HashMap is fine
HashMap<String, String> singleThreaded = new HashMap<>();
// Multi-threaded: Use ConcurrentHashMap or synchronized map
Map<String, String> multiThreaded1 = new ConcurrentHashMap<>();
Map<String, String> multiThreaded2 = Collections.synchronizedMap(new HashMap<>());
System.out.println("Choose the right Map implementation for your concurrency needs");
}
public static void main(String[] args) {
System.out.println("=== HashMap Best Practices ===");
useImmutableKeys();
properEqualsHashCode();
preSizeHashMap();
efficientIteration();
safeValueRetrieval();
threadSafetyConsideration();
}
}
Common Pitfalls to Avoid
import java.util.*;
public class HashMapPitfalls {
// 1. Modifying keys after insertion
public static void modifyingKeys() {
class MutableKey {
String value;
MutableKey(String value) { this.value = value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MutableKey that = (MutableKey) o;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
HashMap<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey("original");
map.put(key, "value");
System.out.println("Before modification: " + map.get(key)); // "value"
key.value = "modified"; // DANGEROUS: Modifying key after insertion
System.out.println("After modification: " + map.get(key)); // null!
System.out.println("The value is still in the map but unreachable!");
}
// 2. Poor hashCode implementation
public static void poorHashCode() {
class BadHashCode {
int id;
BadHashCode(int id) { this.id = id; }
@Override
public int hashCode() {
return 1; // All objects have same hash code!
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BadHashCode that = (BadHashCode) o;
return id == that.id;
}
}
HashMap<BadHashCode, String> map = new HashMap<>();
long startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
map.put(new BadHashCode(i), "Value" + i);
}
long endTime = System.nanoTime();
System.out.println("Time with poor hashCode: " + (endTime - startTime) + " ns");
System.out.println("Performance degrades to O(n)!");
}
// 3. Assuming null safety
public static void nullSafety() {
HashMap<String, String> map = new HashMap<>();
// HashMap allows null keys and null values
map.put(null, "Null key value");
map.put("Key", null);
System.out.println("Value for null key: " + map.get(null)); // "Null key value"
System.out.println("Value for 'Key': " + map.get("Key")); // null
// But this can cause NullPointerExceptions if not handled
String value = map.get("NonExistent");
if (value != null) {
// This block won't execute for non-existent keys
System.out.println("Value length: " + value.length());
}
}
// 4. Concurrent modification during iteration
public static void concurrentModification() {
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
try {
// This will throw ConcurrentModificationException
for (String key : map.keySet()) {
if (key.equals("B")) {
map.remove(key); // Modifying while iterating
}
}
} catch (Exception e) {
System.out.println("ConcurrentModificationException: " + e.getMessage());
}
// Safe way: Use iterator's remove method
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
if (key.equals("B")) {
iterator.remove(); // Safe removal
}
}
System.out.println("Map after safe removal: " + map);
}
public static void main(String[] args) {
System.out.println("=== HashMap Pitfalls to Avoid ===");
modifyingKeys();
poorHashCode();
nullSafety();
concurrentModification();
}
}
Summary
Key Points:
- HashMap provides O(1) average time complexity for get and put operations
- Keys must implement proper equals() and hashCode() methods
- HashMap is not thread-safe - use ConcurrentHashMap for concurrent access
- Load factor (default 0.75) determines when resizing occurs
- Use immutable objects as keys to prevent corruption
- entrySet() iteration is most efficient when both keys and values are needed
When to Use HashMap:
- Fast key-value lookups are required
- Order of elements doesn't matter
- Memory efficiency is important
- You need constant-time performance for basic operations
Alternatives to Consider:
- LinkedHashMap: Maintains insertion order
- TreeMap: Maintains sorted order
- ConcurrentHashMap: Thread-safe version
- HashTable: Legacy thread-safe version (not recommended)
HashMap is one of the most commonly used data structures in Java due to its excellent performance characteristics and flexibility.