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.