πŸ—ΊοΈ Java Map Interface: Complete Guide with Implementations and Examples

πŸ“š Introduction to Maps in Java

Maps are one of the most powerful and frequently used data structures in Java programming. Unlike arrays and lists that store single elements, maps store data in key-value pairs, making them perfect for scenarios where you need to quickly look up values based on a unique identifier.

In Java, Map is an interface in the Collections Framework that represents a mapping between keys and values. Each key is associated with exactly one value, creating what's sometimes called a "dictionary" or "associative array" in other programming languages.

πŸ”‘ Key Characteristics of Java Maps

  • Maps store elements as key-value pairs
  • Each key must be unique within the map
  • Each key maps to exactly one value (though the same value can be mapped by different keys)
  • Maps do not guarantee any specific order of elements (though some implementations do)
  • Maps cannot contain duplicate keys

🧩 The Map Interface Hierarchy in Java

Java Map Interface implentation

The Java Collections Framework provides several implementations of the Map interface, each with different characteristics:

  • HashMap: Fast access, unordered
  • TreeMap: Sorted by keys, slower access
  • LinkedHashMap: Preserves insertion order, moderate access speed
  • Hashtable: Thread-safe legacy implementation (generally replaced by ConcurrentHashMap)
  • ConcurrentHashMap: Thread-safe modern implementation
  • EnumMap: Specialized implementation for enum keys
  • WeakHashMap: Special implementation with weak keys for memory management

Let's dive into the Map interface and explore how to use these powerful data structures in your Java applications!

🧠 Understanding the Java Map Interface

The Map interface defines the core functionality that all map implementations must provide. Let's look at the essential methods:

πŸ“‹ Core Java Map Methods

public interface Map<K, V> {
    // Basic Operations
    V put(K key, V value);           // Associates key with value in the map
    V get(Object key);               // Returns the value associated with key
    V remove(Object key);            // Removes the mapping for key
    boolean containsKey(Object key); // Returns true if map contains key
    boolean containsValue(Object value); // Returns true if map contains value
    int size();                      // Returns the number of key-value mappings
    boolean isEmpty();               // Returns true if map contains no mappings
    void clear();                    // Removes all mappings
    
    // Bulk Operations
    void putAll(Map<? extends K, ? extends V> m); // Copies all mappings from m to this map
    
    // Collection Views
    Set<K> keySet();                 // Returns a Set view of the keys
    Collection<V> values();          // Returns a Collection view of the values
    Set<Map.Entry<K, V>> entrySet(); // Returns a Set view of the mappings
    
    // Map.Entry Interface
    interface Entry<K, V> {
        K getKey();                  // Returns the key
        V getValue();                // Returns the value
        V setValue(V value);         // Replaces the value
    }
}

πŸ”„ Basic Java Map Operations

Let's see these methods in action with a simple example:

import java.util.HashMap;
import java.util.Map;

public class BasicMapOperations {
    public static void main(String[] args) {
        // Create a new HashMap
        Map<String, Integer> studentScores = new HashMap<>();
        
        // Adding elements (key-value pairs)
        studentScores.put("Alice", 95);
        studentScores.put("Bob", 85);
        studentScores.put("Charlie", 90);
        
        System.out.println("Initial Map: " + studentScores);
        
        // Getting a value by key
        int aliceScore = studentScores.get("Alice");
        System.out.println("Alice's score: " + aliceScore);
        
        // Checking if a key exists
        boolean hasDavid = studentScores.containsKey("David");
        System.out.println("Contains David? " + hasDavid);
        
        // Checking if a value exists
        boolean hasScore90 = studentScores.containsValue(90);
        System.out.println("Contains score 90? " + hasScore90);
        
        // Updating a value
        studentScores.put("Bob", 88);  // Overwrites the previous value
        System.out.println("After updating Bob's score: " + studentScores);
        
        // Size of the map
        System.out.println("Number of students: " + studentScores.size());
        
        // Removing an entry
        studentScores.remove("Charlie");
        System.out.println("After removing Charlie: " + studentScores);
        
        // Iterating through the map
        System.out.println("\nStudent Scores:");
        for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // Clearing the map
        studentScores.clear();
        System.out.println("After clearing: " + studentScores);
        System.out.println("Is map empty? " + studentScores.isEmpty());
    }
}

Output:

Initial Map: {Bob=85, Alice=95, Charlie=90}
Alice's score: 95
Contains David? false
Contains score 90? true
After updating Bob's score: {Bob=88, Alice=95, Charlie=90}
Number of students: 3
After removing Charlie: {Bob=88, Alice=95}

Student Scores:
Bob: 88
Alice: 95
After clearing: {}
Is map empty? true

πŸ”„ Iterating Through Maps

There are several ways to iterate through a Map in Java:

import java.util.HashMap;
import java.util.Map;

public class MapIterationDemo {
    public static void main(String[] args) {
        Map<String, String> countryCapitals = new HashMap<>();
        countryCapitals.put("USA", "Washington D.C.");
        countryCapitals.put("UK", "London");
        countryCapitals.put("India", "New Delhi");
        countryCapitals.put("Japan", "Tokyo");
        
        // Method 1: Using entrySet (most efficient)
        System.out.println("Iterating using entrySet:");
        for (Map.Entry<String, String> entry : countryCapitals.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
        
        // Method 2: Using keySet and get
        System.out.println("\nIterating using keySet:");
        for (String country : countryCapitals.keySet()) {
            System.out.println(country + " -> " + countryCapitals.get(country));
        }
        
        // Method 3: Using forEach with lambda (Java 8+)
        System.out.println("\nIterating using forEach:");
        countryCapitals.forEach((country, capital) -> 
            System.out.println(country + " -> " + capital));
        
        // Method 4: Iterating just keys
        System.out.println("\nIterating just keys:");
        for (String country : countryCapitals.keySet()) {
            System.out.println(country);
        }
        
        // Method 5: Iterating just values
        System.out.println("\nIterating just values:");
        for (String capital : countryCapitals.values()) {
            System.out.println(capital);
        }
    }
}

Output:

Iterating using entrySet:
UK -> London
Japan -> Tokyo
USA -> Washington D.C.
India -> New Delhi

Iterating using keySet:
UK -> London
Japan -> Tokyo
USA -> Washington D.C.
India -> New Delhi

Iterating using forEach:
UK -> London
Japan -> Tokyo
USA -> Washington D.C.
India -> New Delhi

Iterating just keys:
UK
Japan
USA
India

Iterating just values:
London
Tokyo
Washington D.C.
New Delhi

πŸ’‘ Choosing the Right Iteration Method

  • entrySet(): Most efficient when you need both keys and values
  • keySet(): Use when you only need keys or when you need to look up values occasionally
  • forEach(): Clean and concise syntax for Java 8+
  • values(): Use when you only need the values

πŸ—οΈ Map Implementations in Java Programming

Now let's explore the different Map implementations in Java and when to use each one.

1️⃣ HashMap in Java

HashMap is the most commonly used Map implementation. It provides constant-time performance for basic operations (get and put) assuming the hash function disperses elements properly.

Key Characteristics:

  • Fast access (O(1) for get/put operations in average case)
  • No guaranteed order of elements
  • Allows one null key and multiple null values
  • Not synchronized (not thread-safe)
import java.util.HashMap;
import java.util.Map;

public class HashMapDemo {
    public static void main(String[] args) {
        // Creating a HashMap
        Map<Integer, String> employeeMap = new HashMap<>();
        
        // Adding elements
        employeeMap.put(1001, "John Smith");
        employeeMap.put(1002, "Emma Watson");
        employeeMap.put(1003, "Robert Brown");
        employeeMap.put(1004, "Julia Roberts");
        
        // HashMap allows null keys and values
        employeeMap.put(null, "Unknown");
        employeeMap.put(1005, null);
        
        System.out.println("Employee Map: " + employeeMap);
        
        // HashMap doesn't maintain insertion order
        employeeMap.put(1006, "New Employee");
        System.out.println("After adding new employee: " + employeeMap);
        
        // Performance demonstration
        long startTime = System.nanoTime();
        String employee = employeeMap.get(1003);
        long endTime = System.nanoTime();
        
        System.out.println("Found employee: " + employee);
        System.out.println("Lookup time: " + (endTime - startTime) + " nanoseconds");
    }
}

Output:

Employee Map: {null=Unknown, 1001=John Smith, 1002=Emma Watson, 1003=Robert Brown, 1004=Julia Roberts, 1005=null}
After adding new employee: {null=Unknown, 1001=John Smith, 1002=Emma Watson, 1003=Robert Brown, 1004=Julia Roberts, 1005=null, 1006=New Employee}
Found employee: Robert Brown
Lookup time: 12345 nanoseconds

2️⃣ LinkedHashMap in Java

LinkedHashMap maintains insertion order or access order (depending on the constructor used) while providing the same performance characteristics as HashMap.

Key Characteristics:

  • Maintains insertion order by default
  • Can be configured to maintain access order (LRU cache)
  • Slightly slower than HashMap due to maintaining the linked list
  • Allows one null key and multiple null values
  • Not synchronized (not thread-safe)
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        // Creating a LinkedHashMap with default insertion-order
        Map<String, Integer> monthDays = new LinkedHashMap<>();
        
        // Adding elements
        monthDays.put("January", 31);
        monthDays.put("February", 28);
        monthDays.put("March", 31);
        monthDays.put("April", 30);
        
        System.out.println("Months in insertion order: " + monthDays);
        
        // Adding more elements
        monthDays.put("May", 31);
        monthDays.put("June", 30);
        
        System.out.println("After adding more months: " + monthDays);
        
        // Creating a LinkedHashMap with access-order (LRU cache behavior)
        // Parameters: initialCapacity, loadFactor, accessOrder
        Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true);
        
        lruCache.put("A", "First");
        lruCache.put("B", "Second");
        lruCache.put("C", "Third");
        
        System.out.println("\nInitial LRU Cache: " + lruCache);
        
        // Accessing elements (will change the order)
        lruCache.get("A");  // Accessing "A" moves it to the end
        
        System.out.println("After accessing 'A': " + lruCache);
        
        lruCache.get("B");  // Accessing "B" moves it to the end
        
        System.out.println("After accessing 'B': " + lruCache);
        
        // Adding a new element
        lruCache.put("D", "Fourth");
        
        System.out.println("After adding 'D': " + lruCache);
    }
}

Output:

Months in insertion order: {January=31, February=28, March=31, April=30}
After adding more months: {January=31, February=28, March=31, April=30, May=31, June=30}

Initial LRU Cache: {A=First, B=Second, C=Third}
After accessing 'A': {B=Second, C=Third, A=First}
After accessing 'B': {C=Third, A=First, B=Second}
After adding 'D': {C=Third, A=First, B=Second, D=Fourth}

3️⃣ TreeMap in Java

TreeMap implements a NavigableMap and stores its entries in a Red-Black tree, ordering them based on their keys.

Key Characteristics:

  • Keys are sorted in natural order or by a provided Comparator
  • Provides guaranteed log(n) time cost for operations
  • Does not allow null keys (will throw NullPointerException)
  • Allows multiple null values
  • Not synchronized (not thread-safe)
import java.util.Map;
import java.util.TreeMap;

public class TreeMapDemo {
    public static void main(String[] args) {
        // Creating a TreeMap (sorted by keys)
        Map<String, Double> stockPrices = new TreeMap<>();
        
        // Adding elements (not in alphabetical order)
        stockPrices.put("GOOGL", 2530.25);
        stockPrices.put("AAPL", 145.85);
        stockPrices.put("MSFT", 280.75);
        stockPrices.put("AMZN", 3305.00);
        
        // TreeMap automatically sorts keys
        System.out.println("Stock prices (sorted by company symbol):");
        for (Map.Entry<String, Double> entry : stockPrices.entrySet()) {
            System.out.println(entry.getKey() + ": $" + entry.getValue());
        }
        
        // TreeMap-specific methods
        TreeMap<String, Double> treeMap = (TreeMap<String, Double>) stockPrices;
        
        // First and last entries
        System.out.println("\nFirst entry: " + treeMap.firstEntry());
        System.out.println("Last entry: " + treeMap.lastEntry());
        
        // Get entries before and after a specific key
        System.out.println("\nEntry before GOOGL: " + treeMap.lowerEntry("GOOGL"));
        System.out.println("Entry after GOOGL: " + treeMap.higherEntry("GOOGL"));
        
        // Submap (range of entries)
        System.out.println("\nSubmap from AAPL to MSFT:");
        Map<String, Double> subMap = treeMap.subMap("AAPL", true, "MSFT", true);
        for (Map.Entry<String, Double> entry : subMap.entrySet()) {
            System.out.println(entry.getKey() + ": $" + entry.getValue());
        }
        
        // Using a custom Comparator (reverse order)
        TreeMap<String, Double> reverseStockMap = new TreeMap<>((s1, s2) -> s2.compareTo(s1));
        reverseStockMap.putAll(stockPrices);
        
        System.out.println("\nStock prices in reverse alphabetical order:");
        for (Map.Entry<String, Double> entry : reverseStockMap.entrySet()) {
            System.out.println(entry.getKey() + ": $" + entry.getValue());
        }
    }
}

Output:

Stock prices (sorted by company symbol):
AAPL: $145.85
AMZN: $3305.0
GOOGL: $2530.25
MSFT: $280.75

First entry: AAPL=$145.85
Last entry: MSFT=$280.75

Entry before GOOGL: AMZN=$3305.0
Entry after GOOGL: MSFT=$280.75

Submap from AAPL to MSFT:
AAPL: $145.85
AMZN: $3305.0
GOOGL: $2530.25
MSFT: $280.75

Stock prices in reverse alphabetical order:
MSFT: $280.75
GOOGL: $2530.25
AMZN: $3305.0
AAPL: $145.85

4️⃣ Hashtable in Java

Hashtable is a legacy class that implements a hash table data structure. It's similar to HashMap but is synchronized (thread-safe).

Key Characteristics:

  • Thread-safe (synchronized)
  • Does not allow null keys or values
  • Generally slower than HashMap due to synchronization
  • No guaranteed order of elements
import java.util.Hashtable;
import java.util.Map;

public class HashtableDemo {
    public static void main(String[] args) {
        // Creating a Hashtable
        Map<String, Integer> population = new Hashtable<>();
        
        // Adding elements
        population.put("New York", 8419000);
        population.put("Los Angeles", 3980000);
        population.put("Chicago", 2716000);
        population.put("Houston", 2328000);
        
        System.out.println("City populations: " + population);
        
        // Hashtable doesn't allow null keys or values
        try {
            population.put(null, 1000000);
        } catch (NullPointerException e) {
            System.out.println("Error: Cannot add null key to Hashtable");
        }
        
        try {
            population.put("Detroit", null);
        } catch (NullPointerException e) {
            System.out.println("Error: Cannot add null value to Hashtable");
        }
        
        // Thread-safe operations
        System.out.println("\nHashtable is thread-safe for concurrent access");
        
        // Using Hashtable-specific methods
        Hashtable<String, Integer> cityTable = (Hashtable<String, Integer>) population;
        
        // Getting an enumeration of the keys
        System.out.println("\nCities in the Hashtable:");
        for (String city : cityTable.keySet()) {
            System.out.println(city + ": " + cityTable.get(city));
        }
    }
}

Output:

City populations: {Chicago=2716000, New York=8419000, Houston=2328000, Los Angeles=3980000}
Error: Cannot add null key to Hashtable
Error: Cannot add null value to Hashtable

Hashtable is thread-safe for concurrent access

Cities in the Hashtable:
Chicago: 2716000
New York: 8419000
Houston: 2328000
Los Angeles: 3980000

5️⃣ ConcurrentHashMap in Java

ConcurrentHashMap is a thread-safe implementation designed for concurrent access. Unlike Hashtable, it doesn't lock the entire map for each operation.

Key Characteristics:

  • Thread-safe with high concurrency
  • Does not allow null keys or values
  • Better performance than Hashtable for concurrent access
  • No guaranteed order of elements
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap
        Map<String, Integer> scores = new ConcurrentHashMap<>();
        
        // Adding elements
        scores.put("Math", 95);
        scores.put("Science", 88);
        scores.put("English", 91);
        scores.put("History", 85);
        
        System.out.println("Subject scores: " + scores);
        
        // ConcurrentHashMap doesn't allow null keys or values
        try {
            scores.put(null, 75);
        } catch (NullPointerException e) {
            System.out.println("Error: Cannot add null key to ConcurrentHashMap");
        }
        
        try {
            scores.put("Art", null);
        } catch (NullPointerException e) {
            System.out.println("Error: Cannot add null value to ConcurrentHashMap");
        }
        
        // ConcurrentHashMap-specific methods
        ConcurrentHashMap<String, Integer> concurrentMap = (ConcurrentHashMap<String, Integer>) scores;
        
        // Atomic operations
        concurrentMap.putIfAbsent("Geography", 82);
        System.out.println("\nAfter putIfAbsent: " + concurrentMap);
        
        // Replace only if current value matches
        concurrentMap.replace("Math", 95, 97);
        System.out.println("After replace: " + concurrentMap);
        
        // Compute a new value (if key exists)
        concurrentMap.compute("Science", (k, v) -> v + 5);
        System.out.println("After compute: " + concurrentMap);
        
        // Compute a new value (if key doesn't exist)
        concurrentMap.computeIfAbsent("Physics", k -> 90);
        System.out.println("After computeIfAbsent: " + concurrentMap);
        
        // Parallel operations (Java 8+)
        System.out.println("\nParallel processing of map entries:");
        concurrentMap.forEach(2, (k, v) -> 
            System.out.println("Processing " + k + " with value " + v + " in thread " + 
                              Thread.currentThread().getName()));
    }
}

Output:

Subject scores: {Science=88, Math=95, History=85, English=91}
Error: Cannot add null key to ConcurrentHashMap
Error: Cannot add null value to ConcurrentHashMap

After putIfAbsent: {Science=88, Geography=82, Math=95, History=85, English=91}
After replace: {Science=88, Geography=82, Math=97, History=85, English=91}
After compute: {Science=93, Geography=82, Math=97, History=85, English=91}
After computeIfAbsent: {Science=93, Geography=82, Math=97, Physics=90, History=85, English=91}

Parallel processing of map entries:
Processing Science with value 93 in thread ForkJoinPool.commonPool-worker-1
Processing Geography with value 82 in thread ForkJoinPool.commonPool-worker-2
Processing Math with value 97 in thread ForkJoinPool.commonPool-worker-3
Processing Physics with value 90 in thread main
Processing History with value 85 in thread ForkJoinPool.commonPool-worker-1
Processing English with value 91 in thread ForkJoinPool.commonPool-worker-2

6️⃣ EnumMap in Java

EnumMap is a specialized Map implementation for use with enum keys. It's highly efficient and maintains the natural order of enum constants.

Key Characteristics:

  • Keys must be of the same enum type
  • Very efficient implementation
  • Maintains the natural order of enum constants
  • Does not allow null keys
  • Not synchronized (not thread-safe)
import java.util.EnumMap;
import java.util.Map;

public class EnumMapDemo {
    // Define an enum for days of the week
    enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }
    
    public static void main(String[] args) {
        // Creating an EnumMap
        EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
        
        // Adding elements
        schedule.put(Day.MONDAY, "Work from office");
        schedule.put(Day.TUESDAY, "Team meeting");
        schedule.put(Day.WEDNESDAY, "Project deadline");
        schedule.put(Day.THURSDAY, "Work from home");
        schedule.put(Day.FRIDAY, "Weekly review");
        schedule.put(Day.SATURDAY, "Weekend plans");
        schedule.put(Day.SUNDAY, "Rest day");
        
        // EnumMap maintains the natural order of enum constants
        System.out.println("Weekly Schedule:");
        for (Map.Entry<Day, String> entry : schedule.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // EnumMap operations
        System.out.println("\nToday's schedule: " + schedule.get(Day.WEDNESDAY));
        
        // Update an entry
        schedule.put(Day.THURSDAY, "Client presentation");
        
        // Remove an entry
        schedule.remove(Day.SATURDAY);
        
        System.out.println("\nUpdated Schedule:");
        for (Map.Entry<Day, String> entry : schedule.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // EnumMap performance
        System.out.println("\nEnumMap is very efficient for enum keys");
        System.out.println("Size of schedule: " + schedule.size());
        System.out.println("Contains FRIDAY? " + schedule.containsKey(Day.FRIDAY));
        System.out.println("Contains SATURDAY? " + schedule.containsKey(Day.SATURDAY));
    }
}

Output:

Weekly Schedule:
MONDAY: Work from office
TUESDAY: Team meeting
WEDNESDAY: Project deadline
THURSDAY: Work from home
FRIDAY: Weekly review
SATURDAY: Weekend plans
SUNDAY: Rest day

Today's schedule: Project deadline

Updated Schedule:
MONDAY: Work from office
TUESDAY: Team meeting
WEDNESDAY: Project deadline
THURSDAY: Client presentation
FRIDAY: Weekly review
SUNDAY: Rest day

EnumMap is very efficient for enum keys
Size of schedule: 6
Contains FRIDAY? true
Contains SATURDAY? false

7️⃣ WeakHashMap in Java

WeakHashMap is a special implementation where the keys are held with weak references, allowing them to be garbage collected if no other strong references exist.

Key Characteristics:

  • Keys are held with weak references
  • Entries are automatically removed when keys are garbage collected
  • Useful for implementing caches
  • No guaranteed order of elements
  • Not synchronized (not thread-safe)
import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        // Creating a WeakHashMap
        Map<Object, String> weakMap = new WeakHashMap<>();
        
        // Creating objects to use as keys
        Object key1 = new Object();
        Object key2 = new Object();
        
        // Adding elements
        weakMap.put(key1, "Value 1");
        weakMap.put(key2, "Value 2");
        
        System.out.println("Initial WeakHashMap: " + weakMap);
        System.out.println("Size: " + weakMap.size());
        
        // Removing the strong reference to key1
        key1 = null;
        
        // Run garbage collection (not guaranteed to run immediately)
        System.gc();
        
        // Sleep to give GC a chance to run
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // Check the map after garbage collection
        System.out.println("\nAfter garbage collection:");
        System.out.println("WeakHashMap: " + weakMap);
        System.out.println("Size: " + weakMap.size());
        
        System.out.println("\nWeakHashMap is useful for implementing memory-sensitive caches");
    }
}

Output:

Initial WeakHashMap: {java.lang.Object@15db9742=Value 1, java.lang.Object@6d06d69c=Value 2}
Size: 2

After garbage collection:
WeakHashMap: {java.lang.Object@6d06d69c=Value 2}
Size: 1

WeakHashMap is useful for implementing memory-sensitive caches

πŸ”„ Advanced Map Operations

Now that we've covered the basic Map implementations, let's explore some advanced operations and techniques.

πŸ”„ Map Merging and Combining

Java 8 introduced several methods for merging and combining maps:

import java.util.HashMap;
import java.util.Map;

public class MapMergingDemo {
    public static void main(String[] args) {
        // Create two maps
        Map<String, Integer> map1 = new HashMap<>();
        map1.put("A", 1);
        map1.put("B", 2);
        map1.put("C", 3);
        
        Map<String, Integer> map2 = new HashMap<>();
        map2.put("B", 20);
        map2.put("C", 30);
        map2.put("D", 40);
        
        System.out.println("Map 1: " + map1);
        System.out.println("Map 2: " + map2);
        
        // Method 1: Using putAll (overwrites duplicate keys)
        Map<String, Integer> combined1 = new HashMap<>(map1);
        combined1.putAll(map2);
        System.out.println("\nCombined using putAll: " + combined1);
        
        // Method 2: Using forEach and merge (Java 8+)
        Map<String, Integer> combined2 = new HashMap<>(map1);
        map2.forEach((key, value) -> 
            combined2.merge(key, value, (oldValue, newValue) -> oldValue + newValue));
        System.out.println("Combined using merge (values added): " + combined2);
        
        // Method 3: Using Stream API (Java 8+)
        Map<String, Integer> combined3 = new HashMap<>();
        
        // Merge both maps into a new map, resolving conflicts by taking the max value
        Stream.of(map1, map2)
              .flatMap(map -> map.entrySet().stream())
              .forEach(entry -> 
                  combined3.merge(entry.getKey(), entry.getValue(), 
                                 (oldValue, newValue) -> Math.max(oldValue, newValue)));
        
        System.out.println("Combined using streams (max value): " + combined3);
    }
}

Output:

Map 1: {A=1, B=2, C=3}
Map 2: {B=20, C=30, D=40}

Combined using putAll: {A=1, B=20, C=30, D=40}
Combined using merge (values added): {A=1, B=22, C=33, D=40}
Combined using streams (max value): {A=1, B=20, C=30, D=40}

πŸ”„ Filtering Maps

Filtering maps is a common operation, especially with Java 8 streams:

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class MapFilteringDemo {
    public static void main(String[] args) {
        // Create a map of products and prices
        Map<String, Double> products = new HashMap<>();
        products.put("Laptop", 999.99);
        products.put("Phone", 699.99);
        products.put("Tablet", 349.99);
        products.put("Watch", 199.99);
        products.put("Headphones", 149.99);
        products.put("Keyboard", 89.99);
        products.put("Mouse", 49.99);
        
        System.out.println("All products: " + products);
        
        // Method 1: Filtering using streams (Java 8+)
        Map<String, Double> expensiveProducts = products.entrySet().stream()
            .filter(entry -> entry.getValue() > 300)
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        
        System.out.println("\nExpensive products (>$300): " + expensiveProducts);
        
        // Method 2: Filtering using forEach and conditional logic
        Map<String, Double> cheapProducts = new HashMap<>();
        products.forEach((product, price) -> {
            if (price < 100) {
                cheapProducts.put(product, price);
            }
        });
        
        System.out.println("Cheap products (<$100): " + cheapProducts);
        
        // Method 3: Filtering and transforming in one operation
        Map<String, String> formattedPrices = products.entrySet().stream()
            .filter(entry -> entry.getValue() >= 100 && entry.getValue() <= 500)
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                entry -> "$" + entry.getValue()
            ));
        
        System.out.println("Mid-range products with formatted prices: " + formattedPrices);
    }
}

Output:

All products: {Tablet=349.99, Keyboard=89.99, Phone=699.99, Laptop=999.99, Headphones=149.99, Mouse=49.99, Watch=199.99}

Expensive products (>$300): {Tablet=349.99, Phone=699.99, Laptop=999.99}
Cheap products (<$100): {Keyboard=89.99, Mouse=49.99}
Mid-range products with formatted prices: {Tablet=$349.99, Headphones=$149.99, Watch=$199.99}

πŸ”„ Sorting Maps

Maps don't have a natural order (except for TreeMap), but we can sort them based on keys or values:

import java.util.*;
import java.util.stream.Collectors;

public class MapSortingDemo {
    public static void main(String[] args) {
        // Create a map of countries and their populations
        Map<String, Integer> countryPopulation = new HashMap<>();
        countryPopulation.put("USA", 331000000);
        countryPopulation.put("India", 1380000000);
        countryPopulation.put("China", 1410000000);
        countryPopulation.put("Brazil", 212000000);
        countryPopulation.put("Japan", 126000000);
        
        System.out.println("Original map: " + countryPopulation);
        
        // Method 1: Sort by keys using TreeMap
        Map<String, Integer> sortedByKey = new TreeMap<>(countryPopulation);
        
        System.out.println("\nSorted by country name: " + sortedByKey);
        
        // Method 2: Sort by values (Java 8+)
        // Convert to list of entries, sort, and collect to LinkedHashMap to preserve order
        Map<String, Integer> sortedByPopulation = countryPopulation.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 population (ascending): " + sortedByPopulation);
        
        // Method 3: Sort by values in descending order
        Map<String, Integer> sortedByPopulationDesc = countryPopulation.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (e1, e2) -> e1,
                LinkedHashMap::new
            ));
        
        System.out.println("Sorted by population (descending): " + sortedByPopulationDesc);
        
        // Method 4: Sort by multiple criteria
        // Create a map of students with name and scores in different subjects
        Map<String, Map<String, Integer>> studentScores = new HashMap<>();
        
        Map<String, Integer> aliceScores = new HashMap<>();
        aliceScores.put("Math", 95);
        aliceScores.put("Science", 92);
        aliceScores.put("English", 88);
        
        Map<String, Integer> bobScores = new HashMap<>();
        bobScores.put("Math", 85);
        bobScores.put("Science", 95);
        bobScores.put("English", 90);
        
        Map<String, Integer> charlieScores = new HashMap<>();
        charlieScores.put("Math", 90);
        charlieScores.put("Science", 88);
        charlieScores.put("English", 95);
        
        studentScores.put("Alice", aliceScores);
        studentScores.put("Bob", bobScores);
        studentScores.put("Charlie", charlieScores);
        
        // Sort students by their average score
        Map<String, Double> averageScores = new HashMap<>();
        
        for (Map.Entry<String, Map<String, Integer>> entry : studentScores.entrySet()) {
            String student = entry.getKey();
            Map<String, Integer> scores = entry.getValue();
            
            double average = scores.values().stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0.0);
            
            averageScores.put(student, average);
        }
        
        // Sort by average score (descending)
        Map<String, Double> sortedByAverage = averageScores.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (e1, e2) -> e1,
                LinkedHashMap::new
            ));
        
        System.out.println("\nStudents sorted by average score:");
        for (Map.Entry<String, Double> entry : sortedByAverage.entrySet()) {
            System.out.printf("%s: %.2f%n", entry.getKey(), entry.getValue());
        }
    }
}

Output:

Original map: {USA=331000000, China=1410000000, Brazil=212000000, Japan=126000000, India=1380000000}

Sorted by country name: {Brazil=212000000, China=1410000000, India=1380000000, Japan=126000000, USA=331000000}
Sorted by population (ascending): {Japan=126000000, Brazil=212000000, USA=331000000, India=1380000000, China=1410000000}
Sorted by population (descending): {China=1410000000, India=1380000000, USA=331000000, Brazil=212000000, Japan=126000000}

Students sorted by average score:
Alice: 91.67
Charlie: 91.00
Bob: 90.00

πŸ”„ Java 8+ Map Enhancements

Java 8 introduced several new methods to the Map interface that make working with maps more convenient:

import java.util.HashMap;
import java.util.Map;

public class MapEnhancementsDemo {
    public static void main(String[] args) {
        Map<String, Integer> inventory = new HashMap<>();
        inventory.put("Apple", 50);
        inventory.put("Banana", 30);
        inventory.put("Orange", 40);
        
        System.out.println("Initial inventory: " + inventory);
        
        // 1. getOrDefault - Returns the value or a default if key not found
        int mangoCount = inventory.getOrDefault("Mango", 0);
        System.out.println("\nMango count: " + mangoCount);
        
        // 2. putIfAbsent - Only puts if key doesn't exist
        inventory.putIfAbsent("Apple", 100);  // Won't change existing value
        inventory.putIfAbsent("Mango", 20);   // Will add new entry
        
        System.out.println("After putIfAbsent: " + inventory);
        
        // 3. computeIfAbsent - Compute value if key is absent
        inventory.computeIfAbsent("Pear", k -> 25);
        System.out.println("After computeIfAbsent: " + inventory);
        
        // 4. computeIfPresent - Compute value if key is present
        inventory.computeIfPresent("Apple", (k, v) -> v + 10);
        System.out.println("After computeIfPresent: " + inventory);
        
        // 5. compute - Compute a new value (present or not)
        inventory.compute("Banana", (k, v) -> (v == null) ? 1 : v * 2);
        System.out.println("After compute: " + inventory);
        
        // 6. merge - Merge with existing value or put if absent
        inventory.merge("Orange", 5, (oldValue, newValue) -> oldValue + newValue);
        inventory.merge("Grape", 15, (oldValue, newValue) -> oldValue + newValue);
        
        System.out.println("After merge: " + inventory);
        
        // 7. forEach - Iterate with BiConsumer
        System.out.println("\nInventory status:");
        inventory.forEach((fruit, count) -> {
            String status = count < 30 ? "Low" : "OK";
            System.out.println(fruit + ": " + count + " (" + status + ")");
        });
        
        // 8. replaceAll - Replace all values
        inventory.replaceAll((k, v) -> v - 5);
        System.out.println("\nAfter reducing all by 5: " + inventory);
        
        // 9. remove with key and value
        boolean removed = inventory.remove("Mango", 15);  // Only removes if value matches
        System.out.println("\nRemoved Mango with value 15? " + removed);
        
        removed = inventory.remove("Mango", 15);
        System.out.println("Removed Mango with value 15? " + removed);
        
        // 10. replace with old value check
        boolean replaced = inventory.replace("Apple", 55, 60);
        System.out.println("\nReplaced Apple 55 with 60? " + replaced);
        
        replaced = inventory.replace("Apple", 55, 60);
        System.out.println("Replaced Apple 55 with 60? " + replaced);
        
        System.out.println("Final inventory: " + inventory);
    }
}

Output:

Initial inventory: {Apple=50, Orange=40, Banana=30}

Mango count: 0
After putIfAbsent: {Apple=50, Mango=20, Orange=40, Banana=30}
After computeIfAbsent: {Apple=50, Mango=20, Orange=40, Pear=25, Banana=30}
After computeIfPresent: {Apple=60, Mango=20, Orange=40, Pear=25, Banana=30}
After compute: {Apple=60, Mango=20, Orange=40, Pear=25, Banana=60}
After merge: {Apple=60, Mango=20, Grape=15, Orange=45, Pear=25, Banana=60}

Inventory status:
Apple: 60 (OK)
Mango: 20 (Low)
Grape: 15 (Low)
Orange: 45 (OK)
Pear: 25 (Low)
Banana: 60 (OK)

After reducing all by 5: {Apple=55, Mango=15, Grape=10, Orange=40, Pear=20, Banana=55}

Removed Mango with value 15? true
Removed Mango with value 15? false

Replaced Apple 55 with 60? true
Replaced Apple 55 with 60? false
Final inventory: {Apple=60, Grape=10, Orange=40, Pear=20, Banana=55}

🚫 Java Maps: Common Pitfalls and Gotchas

When working with Maps in Java, there are several common mistakes and issues to be aware of:

1️⃣ Null Keys and Values

Different Map implementations have different policies regarding null keys and values:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class MapNullHandlingDemo {
    public static void main(String[] args) {
        System.out.println("Null handling in different Map implementations:\n");
        
        // HashMap - allows one null key and multiple null values
        try {
            Map<String, String> hashMap = new HashMap<>();
            hashMap.put(null, "Null Key Value");
            hashMap.put("NullValue", null);
            System.out.println("HashMap with null: " + hashMap);
        } catch (Exception e) {
            System.out.println("HashMap error: " + e.getMessage());
        }
        
        // LinkedHashMap - allows one null key and multiple null values
        try {
            Map<String, String> linkedHashMap = new LinkedHashMap<>();
            linkedHashMap.put(null, "Null Key Value");
            linkedHashMap.put("NullValue", null);
            System.out.println("LinkedHashMap with null: " + linkedHashMap);
        } catch (Exception e) {
            System.out.println("LinkedHashMap error: " + e.getMessage());
        }
        
        // TreeMap - does NOT allow null keys, allows null values
        try {
            Map<String, String> treeMap = new TreeMap<>();
            treeMap.put(null, "Null Key Value");
            System.out.println("TreeMap with null key: " + treeMap);
        } catch (Exception e) {
            System.out.println("TreeMap null key error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
        
        try {
            Map<String, String> treeMap = new TreeMap<>();
            treeMap.put("NullValue", null);
            System.out.println("TreeMap with null value: " + treeMap);
        } catch (Exception e) {
            System.out.println("TreeMap null value error: " + e.getMessage());
        }
        
        // Hashtable - does NOT allow null keys or values
        try {
            Map<String, String> hashtable = new Hashtable<>();
            hashtable.put(null, "Null Key Value");
            System.out.println("Hashtable with null key: " + hashtable);
        } catch (Exception e) {
            System.out.println("Hashtable null key error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
        
        try {
            Map<String, String> hashtable = new Hashtable<>();
            hashtable.put("NullValue", null);
            System.out.println("Hashtable with null value: " + hashtable);
        } catch (Exception e) {
            System.out.println("Hashtable null value error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
        
        // ConcurrentHashMap - does NOT allow null keys or values
        try {
            Map<String, String> concurrentMap = new ConcurrentHashMap<>();
            concurrentMap.put(null, "Null Key Value");
            System.out.println("ConcurrentHashMap with null key: " + concurrentMap);
        } catch (Exception e) {
            System.out.println("ConcurrentHashMap null key error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
        
        try {
            Map<String, String> concurrentMap = new ConcurrentHashMap<>();
            concurrentMap.put("NullValue", null);
            System.out.println("ConcurrentHashMap with null value: " + concurrentMap);
        } catch (Exception e) {
            System.out.println("ConcurrentHashMap null value error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
    }
}

Output:

Null handling in different Map implementations:

HashMap with null: {null=Null Key Value, NullValue=null}
LinkedHashMap with null: {null=Null Key Value, NullValue=null}
TreeMap null key error: NullPointerException - Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is null
TreeMap with null value: {NullValue=null}
Hashtable null key error: NullPointerException - Cannot invoke "Object.hashCode()" because "key" is null
Hashtable null value error: NullPointerException - Cannot invoke "Object.hashCode()" because "value" is null
ConcurrentHashMap null key error: NullPointerException - Cannot invoke "Object.hashCode()" because "key" is null
ConcurrentHashMap null value error: NullPointerException - Cannot invoke "Object.hashCode()" because "value" is null

2️⃣ Mutable Keys

Using mutable objects as keys can lead to unexpected behavior:

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class MutableKeyDemo {
    // A mutable class
    static class MutableKey {
        private String value;
        
        public MutableKey(String value) {
            this.value = value;
        }
        
        public String getValue() {
            return value;
        }
        
        public void setValue(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);
        }
        
        @Override
        public String toString() {
            return "MutableKey{" + value + '}';
        }
    }
    
    // An immutable class
    static class ImmutableKey {
        private final String value;
        
        public ImmutableKey(String value) {
            this.value = value;
        }
        
        public String getValue() {
            return value;
        }
        
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ImmutableKey that = (ImmutableKey) o;
            return Objects.equals(value, that.value);
        }
        
        @Override
        public int hashCode() {
            return Objects.hash(value);
        }
        
        @Override
        public String toString() {
            return "ImmutableKey{" + value + '}';
        }
    }
    
    public static void main(String[] args) {
        // Using a mutable key
        Map<MutableKey, String> mutableMap = new HashMap<>();
        MutableKey key1 = new MutableKey("key1");
        
        mutableMap.put(key1, "Original Value");
        System.out.println("Map with mutable key: " + mutableMap);
        System.out.println("Value for key1: " + mutableMap.get(key1));
        
        // Modify the key after adding to map
        key1.setValue("modifiedKey1");
        System.out.println("\nAfter modifying the key:");
        System.out.println("Map with modified key: " + mutableMap);
        System.out.println("Value for original key: " + mutableMap.get(key1));
        
        // Try to retrieve with a new key with the same value
        MutableKey sameValueKey = new MutableKey("modifiedKey1");
        System.out.println("Value for new key with same value: " + mutableMap.get(sameValueKey));
        
        // Try to retrieve with a key having the original value
        MutableKey originalValueKey = new MutableKey("key1");
        System.out.println("Value for key with original value: " + mutableMap.get(originalValueKey));
        
        System.out.println("\nProblem: The value is now effectively lost in the map because the key's hash code changed!");
        
        // Using an immutable key (best practice)
        Map<ImmutableKey, String> immutableMap = new HashMap<>();
        ImmutableKey immutableKey = new ImmutableKey("key1");
        
        immutableMap.put(immutableKey, "Immutable Key Value");
        System.out.println("\nMap with immutable key: " + immutableMap);
        
        // Cannot modify the immutable key
        // immutableKey.setValue("modified"); // This would cause a compilation error
        
        // Create a new key with the same value
        ImmutableKey sameImmutableValue = new ImmutableKey("key1");
        System.out.println("Value for new immutable key with same value: " + 
                          immutableMap.get(sameImmutableValue));
    }
}

Output:

Map with mutable key: {MutableKey{key1}=Original Value}
Value for key1: Original Value

After modifying the key:
Map with modified key: {MutableKey{modifiedKey1}=Original Value}
Value for original key: Original Value
Value for new key with same value: Original Value
Value for key with original value: null

Problem: The value is now effectively lost in the map because the key's hash code changed!

Map with immutable key: {ImmutableKey{key1}=Immutable Key Value}
Value for new immutable key with same value: Immutable Key Value

3️⃣ ConcurrentModificationException

Modifying a map while iterating can cause ConcurrentModificationException:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentModificationDemo {
    public static void main(String[] args) {
        // Create a map of items to remove
        Map<String, String> items = new HashMap<>();
        items.put("item1", "value1");
        items.put("item2", "value2");
        items.put("item3", "value3");
        items.put("remove1", "value4");
        items.put("remove2", "value5");
        
        System.out.println("Original map: " + items);
        
        // Incorrect way: Modifying while iterating
        try {
            System.out.println("\nTrying to remove items while iterating (incorrect way):");
            for (Map.Entry<String, String> entry : items.entrySet()) {
                String key = entry.getKey();
                if (key.startsWith("remove")) {
                    System.out.println("Trying to remove: " + key);
                    items.remove(key);  // This will cause ConcurrentModificationException
                }
            }
        } catch (Exception e) {
            System.out.println("Error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
        
        // Correct way 1: Using Iterator's remove method
        items = new HashMap<>();
        items.put("item1", "value1");
        items.put("item2", "value2");
        items.put("item3", "value3");
        items.put("remove1", "value4");
        items.put("remove2", "value5");
        
        System.out.println("\nRemoving items using Iterator (correct way 1):");
        Iterator<Map.Entry<String, String>> iterator = items.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            if (entry.getKey().startsWith("remove")) {
                System.out.println("Removing: " + entry.getKey());
                iterator.remove();  // Safe way to remove during iteration
            }
        }
        System.out.println("Map after removal: " + items);
        
        // Correct way 2: Using removeIf (Java 8+)
        items = new HashMap<>();
        items.put("item1", "value1");
        items.put("item2", "value2");
        items.put("item3", "value3");
        items.put("remove1", "value4");
        items.put("remove2", "value5");
        
        System.out.println("\nRemoving items using removeIf (correct way 2):");
        items.entrySet().removeIf(entry -> {
            boolean shouldRemove = entry.getKey().startsWith("remove");
            if (shouldRemove) {
                System.out.println("Removing: " + entry.getKey());
            }
            return shouldRemove;
        });
        System.out.println("Map after removal: " + items);
        
        // Correct way 3: Using ConcurrentHashMap
        Map<String, String> concurrentItems = new ConcurrentHashMap<>();
        concurrentItems.put("item1", "value1");
        concurrentItems.put("item2", "value2");
        concurrentItems.put("item3", "value3");
        concurrentItems.put("remove1", "value4");
        concurrentItems.put("remove2", "value5");
        
        System.out.println("\nRemoving items from ConcurrentHashMap (correct way 3):");
        for (Map.Entry<String, String> entry : concurrentItems.entrySet()) {
            if (entry.getKey().startsWith("remove")) {
                System.out.println("Removing: " + entry.getKey());
                concurrentItems.remove(entry.getKey());  // Safe with ConcurrentHashMap
            }
        }
        System.out.println("Map after removal: " + concurrentItems);
        
        // Correct way 4: Creating a copy of keys to remove
        items = new HashMap<>();
        items.put("item1", "value1");
        items.put("item2", "value2");
        items.put("item3", "value3");
        items.put("remove1", "value4");
        items.put("remove2", "value5");
        
        System.out.println("\nRemoving items using a copy of keys (correct way 4):");
        // Create a list of keys to remove
        List<String> keysToRemove = new ArrayList<>();
        for (String key : items.keySet()) {
            if (key.startsWith("remove")) {
                keysToRemove.add(key);
            }
        }
        
        // Remove the keys
        for (String key : keysToRemove) {
            System.out.println("Removing: " + key);
            items.remove(key);
        }
        System.out.println("Map after removal: " + items);
    }
}

Output:

Original map: {item1=value1, item2=value2, item3=value3, remove1=value4, remove2=value5}

Trying to remove items while iterating (incorrect way):
Trying to remove: remove1
Error: ConcurrentModificationException - null

Removing items using Iterator (correct way 1):
Removing: remove1
Removing: remove2
Map after removal: {item1=value1, item2=value2, item3=value3}

Removing items using removeIf (correct way 2):
Removing: remove1
Removing: remove2
Map after removal: {item1=value1, item2=value2, item3=value3}

Removing items from ConcurrentHashMap (correct way 3):
Removing: remove1
Removing: remove2
Map after removal: {item1=value1, item2=value2, item3=value3}

Removing items using a copy of keys (correct way 4):
Removing: remove1
Removing: remove2
Map after removal: {item1=value1, item2=value2, item3=value3}

4️⃣ Performance Considerations

Different Map implementations have different performance characteristics:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class MapPerformanceDemo {
    private static final int ITERATIONS = 1_000_000;
    
    public static void main(String[] args) {
        // Create different map implementations
        Map<Integer, String> hashMap = new HashMap<>();
        Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
        Map<Integer, String> treeMap = new TreeMap<>();
        Map<Integer, String> hashtable = new Hashtable<>();
        Map<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
        
        // Test insertion performance
        System.out.println("Insertion Performance (adding " + ITERATIONS + " entries):");
        testInsertion(hashMap, "HashMap");
        testInsertion(linkedHashMap, "LinkedHashMap");
        testInsertion(treeMap, "TreeMap");
        testInsertion(hashtable, "Hashtable");
        testInsertion(concurrentHashMap, "ConcurrentHashMap");
        
        // Test lookup performance
        System.out.println("\nLookup Performance (random access " + ITERATIONS + " times):");
        testLookup(hashMap, "HashMap");
        testLookup(linkedHashMap, "LinkedHashMap");
        testLookup(treeMap, "TreeMap");
        testLookup(hashtable, "Hashtable");
        testLookup(concurrentHashMap, "ConcurrentHashMap");
        
        // Test iteration performance
        System.out.println("\nIteration Performance (iterating through all entries):");
        testIteration(hashMap, "HashMap");
        testIteration(linkedHashMap, "LinkedHashMap");
        testIteration(treeMap, "TreeMap");
        testIteration(hashtable, "Hashtable");
        testIteration(concurrentHashMap, "ConcurrentHashMap");
        
        // Performance summary
        System.out.println("\nPerformance Summary:");
        System.out.println("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”");
        System.out.println("β”‚ Operation       β”‚ HashMap   β”‚ LinkedHM  β”‚ TreeMap   β”‚ ConcurHM  β”‚");
        System.out.println("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€");
        System.out.println("β”‚ Insertion       β”‚ O(1)      β”‚ O(1)      β”‚ O(log n)  β”‚ O(1)      β”‚");
        System.out.println("β”‚ Lookup          β”‚ O(1)      β”‚ O(1)      β”‚ O(log n)  β”‚ O(1)      β”‚");
        System.out.println("β”‚ Deletion        β”‚ O(1)      β”‚ O(1)      β”‚ O(log n)  β”‚ O(1)      β”‚");
        System.out.println("β”‚ Iteration       β”‚ O(n)      β”‚ O(n)      β”‚ O(n)      β”‚ O(n)      β”‚");
        System.out.println("β”‚ Memory Overhead β”‚ Medium    β”‚ High      β”‚ High      β”‚ High      β”‚");
        System.out.println("β”‚ Thread Safety   β”‚ No        β”‚ No        β”‚ No        β”‚ Yes       β”‚");
        System.out.println("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜");
    }
    
    private static void testInsertion(Map<Integer, String> map, String mapName) {
        map.clear();
        long startTime = System.nanoTime();
        
        for (int i = 0; i < ITERATIONS; i++) {
            map.put(i, "Value" + i);
        }
        
        long endTime = System.nanoTime();
        long duration = (endTime - startTime) / 1_000_000;  // Convert to milliseconds
        
        System.out.printf("%-20s %8d ms%n", mapName + ":", duration);
    }
    
    private static void testLookup(Map<Integer, String> map, String mapName) {
        // Ensure the map is populated
        if (map.isEmpty()) {
            for (int i = 0; i < ITERATIONS; i++) {
                map.put(i, "Value" + i);
            }
        }
        
        long startTime = System.nanoTime();
        
        // Perform random lookups
        Random random = new Random(42);  // Fixed seed for reproducibility
        for (int i = 0; i < ITERATIONS; i++) {
            int key = random.nextInt(ITERATIONS);
            String value = map.get(key);
        }
        
        long endTime = System.nanoTime();
        long duration = (endTime - startTime) / 1_000_000;  // Convert to milliseconds
        
        System.out.printf("%-20s %8d ms%n", mapName + ":", duration);
    }
    
    private static void testIteration(Map<Integer, String> map, String mapName) {
        // Ensure the map is populated
        if (map.isEmpty()) {
            for (int i = 0; i < ITERATIONS; i++) {
                map.put(i, "Value" + i);
            }
        }
        
        long startTime = System.nanoTime();
        
        // Iterate through all entries
        int count = 0;
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            count += entry.getKey() + entry.getValue().length();
        }
        
        long endTime = System.nanoTime();
        long duration = (endTime - startTime) / 1_000_000;  // Convert to milliseconds
        
        System.out.printf("%-20s %8d ms (dummy sum: %d)%n", mapName + ":", duration, count);
    }
}

Sample Output:

Insertion Performance (adding 1,000,000 entries):
HashMap:                 267 ms
LinkedHashMap:           301 ms
TreeMap:                1089 ms
Hashtable:               412 ms
ConcurrentHashMap:       289 ms

Lookup Performance (random access 1,000,000 times):
HashMap:                 112 ms
LinkedHashMap:           115 ms
TreeMap:                 498 ms
Hashtable:               187 ms
ConcurrentHashMap:       124 ms

Iteration Performance (iterating through all entries):
HashMap:                 143 ms (dummy sum: 1499999500000)
LinkedHashMap:           138 ms (dummy sum: 1499999500000)
TreeMap:                 201 ms (dummy sum: 1499999500000)
Hashtable:               189 ms (dummy sum: 1499999500000)
ConcurrentHashMap:       156 ms (dummy sum: 1499999500000)

Performance Summary:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Operation       β”‚ HashMap   β”‚ LinkedHM  β”‚ TreeMap   β”‚ ConcurHM  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Insertion       β”‚ O(1)      β”‚ O(1)      β”‚ O(log n)  β”‚ O(1)      β”‚
β”‚ Lookup          β”‚ O(1)      β”‚ O(1)      β”‚ O(log n)  β”‚ O(1)      β”‚
β”‚ Deletion        β”‚ O(1)      β”‚ O(1)      β”‚ O(log n)  β”‚ O(1)      β”‚
β”‚ Iteration       β”‚ O(n)      β”‚ O(n)      β”‚ O(n)      β”‚ O(n)      β”‚
β”‚ Memory Overhead β”‚ Medium    β”‚ High      β”‚ High      β”‚ High      β”‚
β”‚ Thread Safety   β”‚ No        β”‚ No        β”‚ No        β”‚ Yes       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”„ Real-World Examples

Let's look at some practical examples of using Maps in real-world scenarios:

1️⃣ Word Frequency Counter

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

public class WordFrequencyCounter {
    public static void main(String[] args) {
        String text = "To be or not to be, that is the question. " +
                      "Whether 'tis nobler in the mind to suffer " +
                      "The slings and arrows of outrageous fortune, " +
                      "Or to take arms against a sea of troubles " +
                      "And by opposing end them.";
        
        // Convert to lowercase and split by non-word characters
        String[] words = text.toLowerCase().split("\\W+");
        
        // Method 1: Using HashMap and manual counting
        Map<String, Integer> wordCounts = new HashMap<>();
        
        for (String word : words) {
            if (!word.isEmpty()) {  // Skip empty strings
                wordCounts.put(word, wordCounts.getOrDefault(word, 0) + 1);
            }
        }
        
        System.out.println("Word frequencies:");
        for (Map.Entry<String, Integer> entry : wordCounts.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // Method 2: Using Java 8 Streams
        Map<String, Long> wordCountsStream = Arrays.stream(words)
            .filter(word -> !word.isEmpty())
            .collect(Collectors.groupingBy(
                word -> word,
                Collectors.counting()
            ));
        
        System.out.println("\nWord frequencies (using streams):");
        wordCountsStream.entrySet().stream()
            .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
            .limit(5)
            .forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
        
        // Method 3: Reading from a file (commented out as it requires a file)
        /*
        try {
            List<String> lines = Files.readAllLines(Paths.get("sample.txt"));
            String fileContent = String.join(" ", lines);
            String[] fileWords = fileContent.toLowerCase().split("\\W+");
            
            Map<String, Long> fileWordCounts = Arrays.stream(fileWords)
                .filter(word -> !word.isEmpty())
                .collect(Collectors.groupingBy(
                    word -> word,
                    Collectors.counting()
                ));
                
            System.out.println("\nTop 10 words in file:");
            fileWordCounts.entrySet().stream()
                .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
                .limit(10)
                .forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
        } catch (IOException e) {
            System.out.println("Error reading file: " + e.getMessage());
        }
        */
    }
}

Output:

Word frequencies:
in: 1
by: 1
sea: 1
arms: 1
that: 1
mind: 1
opposing: 1
fortune: 1
question: 1
whether: 1
tis: 1
take: 1
end: 1
them: 1
and: 2
is: 1
suffer: 1
against: 1
nobler: 1
slings: 1
arrows: 1
troubles: 1
of: 2
outrageous: 1
a: 1
or: 2
not: 1
the: 2
to: 4
be: 2

Word frequencies (using streams):
to: 4
the: 2
and: 2
of: 2
or: 2

2️⃣ Simple Cache Implementation

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SimpleCacheDemo {
    // A simple generic cache implementation
    static class SimpleCache<K, V> {
        private final Map<K, CacheEntry<V>> cache;
        private final long expiryTimeMillis;
        
        public SimpleCache(long expiryTimeMillis) {
            this.cache = new ConcurrentHashMap<>();
            this.expiryTimeMillis = expiryTimeMillis;
        }
        
        public void put(K key, V value) {
            cache.put(key, new CacheEntry<>(value, System.currentTimeMillis() + expiryTimeMillis));
        }
        
        public V get(K key) {
            CacheEntry<V> entry = cache.get(key);
            
            if (entry == null) {
                return null;
            }
            
            // Check if the entry has expired
            if (System.currentTimeMillis() > entry.expiryTime) {
                cache.remove(key);
                return null;
            }
            
            return entry.value;
        }
        
        public void remove(K key) {
            cache.remove(key);
        }
        
        public void clear() {
            cache.clear();
        }
        
        public int size() {
            // Clean up expired entries
            long currentTime = System.currentTimeMillis();
            cache.entrySet().removeIf(entry -> currentTime > entry.getValue().expiryTime);
            
            return cache.size();
        }
        
        // Inner class to hold cache values with expiry time
        private static class CacheEntry<V> {
            private final V value;
            private final long expiryTime;
            
            public CacheEntry(V value, long expiryTime) {
                this.value = value;
                this.expiryTime = expiryTime;
            }
        }
    }
    
    // A slow operation that we want to cache
    static class ExpensiveOperation {
        public String compute(String input) {
            // Simulate a slow operation
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            return "Result for " + input + " computed at " + System.currentTimeMillis();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ExpensiveOperation operation = new ExpensiveOperation();
        SimpleCache<String, String> cache = new SimpleCache<>(5000);  // 5 second expiry
        
        // First call - should be slow
        long startTime = System.currentTimeMillis();
        String result1 = getWithCache("input1", operation, cache);
        long duration1 = System.currentTimeMillis() - startTime;
        System.out.println("First call: " + result1);
        System.out.println("Duration: " + duration1 + "ms");
        
        // Second call - should be fast (cached)
        startTime = System.currentTimeMillis();
        String result2 = getWithCache("input1", operation, cache);
        long duration2 = System.currentTimeMillis() - startTime;
        System.out.println("\nSecond call: " + result2);
        System.out.println("Duration: " + duration2 + "ms");
        
        // Different input - should be slow
        startTime = System.currentTimeMillis();
        String result3 = getWithCache("input2", operation, cache);
        long duration3 = System.currentTimeMillis() - startTime;
        System.out.println("\nDifferent input: " + result3);
        System.out.println("Duration: " + duration3 + "ms");
        
        // Wait for cache to expire
        System.out.println("\nWaiting for cache to expire...");
        Thread.sleep(6000);
        
        // After expiry - should be slow again
        startTime = System.currentTimeMillis();
        String result4 = getWithCache("input1", operation, cache);
        long duration4 = System.currentTimeMillis() - startTime;
        System.out.println("After expiry: " + result4);
        System.out.println("Duration: " + duration4 + "ms");
    }
    
    private static String getWithCache(String input, ExpensiveOperation operation, SimpleCache<String, String> cache) {
        String result = cache.get(input);
        
        if (result == null) {
            System.out.println("Cache miss for " + input + ", computing...");
            result = operation.compute(input);
            cache.put(input, result);
        } else {
            System.out.println("Cache hit for " + input);
        }
        
        return result;
    }
}

Output:

Cache miss for input1, computing...
First call: Result for input1 computed at 1634567890123
Duration: 1005ms

Cache hit for input1
Second call: Result for input1 computed at 1634567890123
Duration: 2ms

Cache miss for input2, computing...
Different input: Result for input2 computed at 1634567891130
Duration: 1003ms

Waiting for cache to expire...
Cache miss for input1, computing...
After expiry: Result for input1 computed at 1634567897135
Duration: 1002ms

3️⃣ Configuration Manager

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class ConfigurationManager {
    private Map<String, String> configMap;
    private final String configFile;
    
    public ConfigurationManager(String configFile) {
        this.configFile = configFile;
        this.configMap = new HashMap<>();
        loadConfiguration();
    }
    
    public void loadConfiguration() {
        Properties properties = new Properties();
        
        try (FileInputStream fis = new FileInputStream(configFile)) {
            properties.load(fis);
            
            // Convert Properties to Map
            for (String key : properties.stringPropertyNames()) {
                configMap.put(key, properties.getProperty(key));
            }
            
            System.out.println("Configuration loaded successfully");
        } catch (IOException e) {
            System.out.println("Could not load configuration: " + e.getMessage());
            // Initialize with default values
            setDefaults();
        }
    }
    
    public void saveConfiguration() {
        Properties properties = new Properties();
        
        // Convert Map to Properties
        for (Map.Entry<String, String> entry : configMap.entrySet()) {
            properties.setProperty(entry.getKey(), entry.getValue());
        }
        
        try (FileOutputStream fos = new FileOutputStream(configFile)) {
            properties.store(fos, "Application Configuration");
            System.out.println("Configuration saved successfully");
        } catch (IOException e) {
            System.out.println("Could not save configuration: " + e.getMessage());
        }
    }
    
    public String get(String key) {
        return configMap.get(key);
    }
    
    public String get(String key, String defaultValue) {
        return configMap.getOrDefault(key, defaultValue);
    }
    
    public void set(String key, String value) {
        configMap.put(key, value);
    }
    
    public boolean getBoolean(String key, boolean defaultValue) {
        String value = get(key);
        return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
    }
    
    public int getInt(String key, int defaultValue) {
        String value = get(key);
        if (value != null) {
            try {
                return Integer.parseInt(value);
            } catch (NumberFormatException e) {
                return defaultValue;
            }
        }
        return defaultValue;
    }
    
    private void setDefaults() {
        configMap.put("app.name", "MyApplication");
        configMap.put("app.version", "1.0");
        configMap.put("ui.theme", "light");
        configMap.put("ui.language", "en");
        configMap.put("network.timeout", "30000");
        configMap.put("debug.enabled", "false");
    }
    
    public static void main(String[] args) {
        ConfigurationManager config = new ConfigurationManager("app.properties");
        
        // Display current configuration
        System.out.println("\nCurrent configuration:");
        System.out.println("App name: " + config.get("app.name"));
        System.out.println("UI theme: " + config.get("ui.theme"));
        System.out.println("Network timeout: " + config.getInt("network.timeout", 5000) + "ms");
        System.out.println("Debug enabled: " + config.getBoolean("debug.enabled", false));
        
        // Update configuration
        System.out.println("\nUpdating configuration...");
        config.set("ui.theme", "dark");
        config.set("network.timeout", "60000");
        config.set("debug.enabled", "true");
        
        // Display updated configuration
        System.out.println("\nUpdated configuration:");
        System.out.println("UI theme: " + config.get("ui.theme"));
        System.out.println("Network timeout: " + config.getInt("network.timeout", 5000) + "ms");
        System.out.println("Debug enabled: " + config.getBoolean("debug.enabled", false));
        
        // Save configuration
        config.saveConfiguration();
    }
}

Output:

Could not load configuration: app.properties (No such file or directory)
Configuration loaded successfully

Current configuration:
App name: MyApplication
UI theme: light
Network timeout: 30000ms
Debug enabled: false

Updating configuration...

Updated configuration:
UI theme: dark
Network timeout: 60000ms
Debug enabled: true
Configuration saved successfully

πŸ“š Summary

The Map interface and its implementations are essential tools in Java programming. Here's a summary of what we've covered:

  1. Map Interface: A key-value pair collection that doesn't allow duplicate keys.

  2. Key Implementations:

    • HashMap: Fast, unordered, allows null keys and values
    • LinkedHashMap: Maintains insertion order, allows null keys and values
    • TreeMap: Sorted by natural order or comparator, doesn't allow null keys
    • Hashtable: Legacy, synchronized, doesn't allow null keys or values
    • ConcurrentHashMap: Thread-safe, high concurrency, doesn't allow null keys or values
    • EnumMap: Specialized for enum keys, very efficient
    • WeakHashMap: Keys held with weak references, useful for caches
  3. Common Operations:

    • Adding elements: put(), putAll(), putIfAbsent()
    • Retrieving elements: get(), getOrDefault()
    • Removing elements: remove()
    • Checking: containsKey(), containsValue(), isEmpty(), size()
    • Iterating: keySet(), values(), entrySet()
  4. Java 8+ Enhancements:

    • Functional operations: compute(), computeIfAbsent(), computeIfPresent()
    • Merging: merge()
    • Iteration: forEach()
    • Replacement: replace(), replaceAll()
  5. Common Pitfalls:

    • Null handling varies by implementation
    • Mutable keys can cause problems
    • ConcurrentModificationException when modifying during iteration
    • Performance characteristics vary by implementation
  6. Real-World Applications:

    • Word frequency counting
    • Caching
    • Configuration management
    • Data indexing
    • Counting and grouping

Maps are versatile data structures that can be used in a wide variety of applications. Understanding the different implementations and their characteristics is crucial for writing efficient and correct Java code.