Java Map Interface — Key-Value Collections

3 minute read

A Map stores key-value pairs where each key is unique. Think of it as a dictionary — look up a word (key) to find its definition (value).


The Map Contract

  1. Unique keys — each key maps to at most one value.
  2. Keys and values can be any object (including null in HashMap).
  3. Map does not extend Collection — it’s a separate hierarchy.
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31);  // overwrites previous value
System.out.println(ages);  // {Alice=31, Bob=25}

Map Implementations

Implementation Order Null Key Null Values Get/Put Backed By
HashMap None ✅ (one) ✅ O(1) Hash table
LinkedHashMap Insertion order ✅ (one) ✅ O(1) Hash table + linked list
TreeMap Sorted by key ❌ ✅ O(log n) Red-black tree
Hashtable None ❌ ❌ O(1) Legacy, synchronized
ConcurrentHashMap None ❌ ❌ O(1) Thread-safe

Essential Methods

Adding and Retrieving

Map<String, String> capitals = new HashMap<>();
capitals.put("France", "Paris");
capitals.put("Japan", "Tokyo");
capitals.put("India", "New Delhi");

capitals.get("France");             // "Paris"
capitals.get("Germany");            // null (not found)
capitals.getOrDefault("Germany", "Unknown"); // "Unknown"

Checking and Removing

capitals.containsKey("Japan");      // true
capitals.containsValue("Tokyo");    // true
capitals.remove("Japan");           // removes and returns "Tokyo"
capitals.remove("France", "London"); // false — value doesn't match
capitals.size();                    // 2
capitals.isEmpty();                 // false

Java 8+ Map Methods

putIfAbsent()

capitals.putIfAbsent("France", "Lyon");
// Does nothing — "France" already exists
// Returns existing value "Paris"

computeIfAbsent()

Compute a value only if the key is absent — perfect for caches:

Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("apple");
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("banana");
// {fruits=[apple, banana]}

compute() and computeIfPresent()

Map<String, Integer> wordCount = new HashMap<>();
String[] words = {"hello", "world", "hello", "java"};

for (String word : words) {
    wordCount.compute(word, (k, v) -> v == null ? 1 : v + 1);
}
// {hello=2, world=1, java=1}

// Or more elegantly:
for (String word : words) {
    wordCount.merge(word, 1, Integer::sum);
}

merge()

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 80);
scores.merge("Alice", 10, Integer::sum);  // 80 + 10 = 90
scores.merge("Bob", 75, Integer::sum);    // new entry: 75

replaceAll()

Map<String, Integer> prices = new HashMap<>(Map.of("A", 100, "B", 200));
prices.replaceAll((key, value) -> value + 10);
// {A=110, B=210}

Iterating Over a Map

1. entrySet() — Key + Value (Most Common)

for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

2. keySet() — Keys Only

for (String name : ages.keySet()) {
    System.out.println(name);
}

3. values() — Values Only

for (Integer age : ages.values()) {
    System.out.println(age);
}

4. forEach() (Java 8+)

ages.forEach((name, age) -> System.out.println(name + ": " + age));

Immutable Maps (Java 9+)

// Up to 10 entries
Map<String, Integer> map = Map.of(
    "A", 1,
    "B", 2,
    "C", 3
);

// Any number of entries
Map<String, Integer> map2 = Map.ofEntries(
    Map.entry("A", 1),
    Map.entry("B", 2),
    Map.entry("C", 3)
);

// Copy of mutable map (Java 10+)
Map<String, Integer> copy = Map.copyOf(mutableMap);

Map.of() does not allow null keys or values, and throws IllegalArgumentException on duplicate keys.


Practical Example: Word Frequency Counter

public Map<String, Long> countWords(String text) {
    return Arrays.stream(text.toLowerCase().split("\\W+"))
            .collect(Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()));
}

// Without streams:
public Map<String, Integer> countWords(String text) {
    Map<String, Integer> freq = new HashMap<>();
    for (String word : text.toLowerCase().split("\\W+")) {
        freq.merge(word, 1, Integer::sum);
    }
    return freq;
}

Choosing the Right Map

Need fastest get/put?
└── HashMap (O(1), no order)

Need insertion order?
└── LinkedHashMap (O(1), ordered)

Need sorted keys?
└── TreeMap (O(log n), sorted)

Need thread safety?
└── ConcurrentHashMap

Need enum keys?
└── EnumMap (most efficient)

Common Mistakes

Mistake Problem Fix
Not overriding hashCode() for custom keys Keys “disappear” from map Override equals() + hashCode()
Mutating a key after putting it Hash changes, entry is lost Use immutable keys
Iterating keySet() then calling get() Double lookup (wasteful) Use entrySet()
Using Hashtable Legacy, slow Use HashMap or ConcurrentHashMap

Key Takeaways

  1. Map = unique keys mapping to values. Not part of Collection.
  2. HashMap is the default — O(1), no ordering guarantee.
  3. Use merge(), computeIfAbsent(), and getOrDefault() for clean code.
  4. Iterate with entrySet() for best performance.
  5. Use Map.of() for small immutable maps.
  6. Always override equals() + hashCode() for custom key types.

What’s Next?

Next: HashMap Deep Dive — hash table internals, buckets, collisions, load factor, and the treeification threshold.

Updated: