HashMap Key-Value Storage in Java: Complete Guide

Table of Contents

  1. Introduction to HashMap
  2. HashMap Internal Structure
  3. Basic Operations
  4. Key Requirements
  5. Advanced Operations
  6. Performance Characteristics
  7. Thread Safety and ConcurrentHashMap
  8. Real-World Examples
  9. 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:

  1. HashMap provides O(1) average time complexity for get and put operations
  2. Keys must implement proper equals() and hashCode() methods
  3. HashMap is not thread-safe - use ConcurrentHashMap for concurrent access
  4. Load factor (default 0.75) determines when resizing occurs
  5. Use immutable objects as keys to prevent corruption
  6. 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.

Leave a Reply

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


Macro Nepal Helper