PrepZone Logo
PrepZone

Working with Maps

Key-value basics, the modern convenience methods, and what load factor means for your code.

Why this matters

  • Maps are the most used data structure in application code, and the convenience methods added in Java 8 remove most of the boilerplate people still write by hand.
  • The check-then-act pattern on a map is both verbose and unsafe under concurrency, and there is a single-call replacement for every instance of it.
  • Load factor and initial capacity are the two tuning knobs that matter, and they are easy to get right once.
table — array of buckets
bucket 0empty
bucket 1"asha" → 30
bucket 5 — collision chain
"ravi" → 24next →
"neha" → 29next → null
bucket 9 — treeified
Red-black treeMore than 8 entries: lookup becomes O(log n)

Bucket index comes from the key's hash, not its insertion order — which is why HashMap iteration has no predictable sequence.

The hash picks a bucket. Keys that land in the same bucket form a short list, and once a bucket grows past eight entries it becomes a balanced tree so lookups stay fast.

The core operations

Java
Map<String, Integer> stock = new HashMap<>();

stock.put("apple", 10);
stock.put("apple", 15);              // replaces — returns the previous value, 10
stock.get("apple");                   // 15
stock.get("banana");                  // null — absent, not an error

stock.containsKey("apple");           // true
stock.containsValue(15);              // true, but O(n) — scans every entry
stock.remove("apple");                // returns 15
stock.size();
stock.isEmpty();

The three views

A map exposes its contents as three views — live windows, not copies.

Java
Map<String, Integer> stock = new HashMap<>(Map.of("apple", 10, "pear", 4));

Set<String> keys = stock.keySet();                        // view of the keys
Collection<Integer> values = stock.values();              // view of the values
Set<Map.Entry<String, Integer>> entries = stock.entrySet();  // view of the pairs

keys.remove("apple");                 // removes the entry from the MAP
System.out.println(stock);             // {pear=4}

Because they are views, removing from keySet() removes from the map. Adding is not supported, since a key with no value is meaningless.

Iterating by entry is both clearer and faster than looking each value up again:

Java
// Preferred: one pass, no repeated lookups
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

// Wasteful: a hash lookup per iteration
for (String key : stock.keySet()) {
    System.out.println(key + " = " + stock.get(key));
}

// Cleanest for a simple pass
stock.forEach((key, value) -> System.out.println(key + " = " + value));

The methods that replace check-then-act

Each of these collapses a pattern that is otherwise three or four lines, and each is atomic on a ConcurrentHashMap.

Java
Map<String, Integer> counts = new HashMap<>();
Map<String, List<String>> groups = new HashMap<>();

// Counting — merge
counts.merge("java", 1, Integer::sum);         // insert 1, or add 1 to what is there

// Grouping — computeIfAbsent
groups.computeIfAbsent("languages", key -> new ArrayList<>()).add("java");

// Reading with a fallback — getOrDefault
int seen = counts.getOrDefault("kotlin", 0);

// Insert only if missing — putIfAbsent
counts.putIfAbsent("go", 0);

// Update only if present — computeIfPresent
counts.computeIfPresent("java", (key, value) -> value * 2);

// Update either way — compute
counts.compute("rust", (key, value) -> value == null ? 1 : value + 1);

// Replace every value in place
counts.replaceAll((key, value) -> value * 10);

// Conditional removal
counts.remove("go", 0);                         // only if the value is currently 0

Compare the before and after on the two most common cases:

Java
// Counting, the old way
Integer current = counts.get("java");
if (current == null) {
    counts.put("java", 1);
} else {
    counts.put("java", current + 1);
}

// Counting, with merge
counts.merge("java", 1, Integer::sum);
Java
// Grouping, the old way
List<String> list = groups.get("languages");
if (list == null) {
    list = new ArrayList<>();
    groups.put("languages", list);
}
list.add("java");

// Grouping, with computeIfAbsent
groups.computeIfAbsent("languages", key -> new ArrayList<>()).add("java");

Which to use when

  • merge — combining a new value with an existing one. Counting and summing.
  • computeIfAbsent — the value is a container you need to create once. Grouping, caching.
  • getOrDefault — reading, where absence has a sensible default.
  • putIfAbsent — initialising without overwriting.
  • compute — the new value depends on the old one, which may be absent.

Capacity and load factor

A HashMap holds an array of buckets. Capacity is the array length, always a power of two. Load factor is how full it is allowed to get before resizing, defaulting to 0.75.

Java
Map<String, String> map = new HashMap<>();              // capacity 16, threshold 12
Map<String, String> sized = new HashMap<>(64);           // capacity 64, threshold 48
Map<String, String> tuned = new HashMap<>(64, 0.5f);     // threshold 32 — fewer collisions, more memory

When size exceeds capacity × load factor, the map doubles its capacity and redistributes every entry. That is an O(n) operation.

Sizing it properly

  • Expecting n entries, pass n / 0.75 + 1 as the initial capacity to avoid any resize.
  • Java 19 added HashMap.newHashMap(n), which does that arithmetic for you.
  • A lower load factor means fewer collisions and more wasted space.
  • A higher load factor means denser buckets, longer chains and slower lookups.
  • 0.75 is a well-chosen default; change it only with a measurement in hand.
Java
// Expecting 1000 entries, with no resizing
Map<String, String> precise = new HashMap<>(1334);
Map<String, String> modern = HashMap.newHashMap(1000);   // Java 19+

Choosing a map

AspectImplementationUse it when
HashMapThe defaultO(1) average, no ordering, one null key allowed
LinkedHashMapOrder mattersInsertion or access order; the basis of an LRU cache
TreeMapSorted keys neededO(log n), with navigation and range queries
ConcurrentHashMapMultiple threadsConcurrent reads, fine-grained write locking, no nulls
EnumMapKeys are enum constantsArray-backed, very fast and compact
HashtableNever, in new codeLegacy; superseded by ConcurrentHashMap
  • HashMap

    ImplementationThe default
    Use it whenO(1) average, no ordering, one null key allowed
  • LinkedHashMap

    ImplementationOrder matters
    Use it whenInsertion or access order; the basis of an LRU cache
  • TreeMap

    ImplementationSorted keys needed
    Use it whenO(log n), with navigation and range queries
  • ConcurrentHashMap

    ImplementationMultiple threads
    Use it whenConcurrent reads, fine-grained write locking, no nulls
  • EnumMap

    ImplementationKeys are enum constants
    Use it whenArray-backed, very fast and compact
  • Hashtable

    ImplementationNever, in new code
    Use it whenLegacy; superseded by ConcurrentHashMap

EnumMap is as overlooked as EnumSet and just as worthwhile.

Keys must be stable

The key's hash code is computed once, at insertion, to choose a bucket. If the key changes afterwards, the entry becomes unreachable.

Java
List<String> key = new ArrayList<>(List.of("a"));
Map<List<String>, String> map = new HashMap<>();
map.put(key, "stored");

key.add("b");                          // the list's hashCode just changed

System.out.println(map.get(key));      // null
System.out.println(map.size());        // 1 — still present, but unreachable

Common misreadings

  • "get returning null means the key is absent." It may be present with a null value. Use containsKey or getOrDefault.
  • "keySet() returns a copy." It is a live view, and removing from it removes from the map.
  • "containsValue is as fast as containsKey." It scans every entry — O(n).
  • "Load factor is a size limit." It is the fullness threshold that triggers doubling.
  • "computeIfAbsent returns the map." It returns the value, existing or newly computed, which is what makes the chained .add(...) work.
  • "HashMap is thread-safe for reads." Only if nothing writes. A concurrent resize can corrupt the structure.

Quick recall

Everything you need if you only revisit this box.

  • get returning null is ambiguous; use containsKey or getOrDefault.
  • keySet, values and entrySet are live views — removing from them changes the map.
  • Iterate with entrySet or forEach, never with keySet plus get.
  • Replace check-then-act with merge (counting), computeIfAbsent (grouping), getOrDefault, putIfAbsent and compute.
  • Returning null from compute or merge removes the entry.
  • Capacity is a power of two; the default load factor 0.75 triggers doubling and a full rehash. Pre-size with n / 0.75 + 1, or HashMap.newHashMap(n) on Java 19+.
  • Keys must be immutable. A key whose hash changes strands its entry permanently.

Test yourself

Answer these before moving on — recall is what makes it stick.