πΊοΈ 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
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:
-
Map Interface: A key-value pair collection that doesn't allow duplicate keys.
-
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
-
Common Operations:
- Adding elements:
put()
,putAll()
,putIfAbsent()
- Retrieving elements:
get()
,getOrDefault()
- Removing elements:
remove()
- Checking:
containsKey()
,containsValue()
,isEmpty()
,size()
- Iterating:
keySet()
,values()
,entrySet()
- Adding elements:
-
Java 8+ Enhancements:
- Functional operations:
compute()
,computeIfAbsent()
,computeIfPresent()
- Merging:
merge()
- Iteration:
forEach()
- Replacement:
replace()
,replaceAll()
- Functional operations:
-
Common Pitfalls:
- Null handling varies by implementation
- Mutable keys can cause problems
- ConcurrentModificationException when modifying during iteration
- Performance characteristics vary by implementation
-
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.