RizTech Academy logo
RizTech Academy
CollectionsLesson 3 of 730 min

Map and HashMap

Map answers one question: what is the value for this key? Counting things, grouping things, caching things and looking things up are all that question, and together they are most of what a backend does.

It is also the collection with the most methods worth knowing, because four lines of get, null check and put collapse into one — and the one-line version is harder to get wrong.

The basics

Map<String, Integer> tiffins = new HashMap<>();
tiffins.put("Priya", 26);
tiffins.put("Arjun", 18);
tiffins.put("Priya", 30);

System.out.println(tiffins);
System.out.println(tiffins.size());
System.out.println(tiffins.get("Nobody"));
System.out.println(tiffins.getOrDefault("Nobody", 0));
{Priya=30, Arjun=18}
2
null
0

Keys are unique. The second put("Priya", ...) replaced the value rather than adding an entry — and put returns the old value, which is occasionally useful and usually ignored.

A missing key gives null, not an exception. That null is the source of more NullPointerExceptions than anything else in Java, because it unboxes:

int count = tiffins.get("Nobody");   // NullPointerException at runtime

getOrDefault exists precisely for this and should be your default.

The full method set

Method Does
put(k, v) Adds or replaces. Returns the old value or null
get(k) Value or null
getOrDefault(k, d) Value or d — prefer this
putIfAbsent(k, v) Only if the key has no value
containsKey(k) / containsValue(v) containsValue scans everything
remove(k) Removes, returns the old value
size() / isEmpty() / clear()
keySet() / values() / entrySet() Views, backed by the map
merge(k, v, fn) Insert v, or combine with the existing value
compute(k, fn) Recompute from the key and current value
computeIfAbsent(k, fn) Build a value only if missing
computeIfPresent(k, fn) Only if already there
forEach((k, v) -> ...) Iterate both halves
Map.of(k, v, ...) Immutable, up to 10 pairs, rejects nulls
Map.entry(k, v) One pair, for Map.ofEntries

The views matter: keySet() is not a copy, so map.keySet().remove(k) removes the entry from the map.

Counting, four ways

The single most common thing you will do with a map. All four give the same answer:

String[] plans = {"veg", "jain", "veg", "veg", "jain"};
// 1. By hand — the version everyone writes first
Map<String, Integer> byHand = new HashMap<>();
for (String p : plans) {
    Integer current = byHand.get(p);
    byHand.put(p, current == null ? 1 : current + 1);
}

// 2. getOrDefault — shorter and no null handling
Map<String, Integer> withDefault = new HashMap<>();
for (String p : plans) {
    withDefault.put(p, withDefault.getOrDefault(p, 0) + 1);
}

// 3. merge — say what to do when the key is already there
Map<String, Integer> merged = new HashMap<>();
for (String p : plans) {
    merged.merge(p, 1, Integer::sum);
}
{veg=3, jain=2}

The fourth is Collectors.groupingBy with counting(), which is module 6.

merge(key, 1, Integer::sum) is the one to learn. It reads as "put 1, or if something is there, add them" and there is no null to forget.

Grouping with computeIfAbsent

The other everyday pattern: a map whose values are lists.

Map<String, List<String>> byArea = new HashMap<>();
byArea.computeIfAbsent("Wagholi", k -> new ArrayList<>()).add("Priya");
byArea.computeIfAbsent("Wagholi", k -> new ArrayList<>()).add("Arjun");
byArea.computeIfAbsent("Kharadi", k -> new ArrayList<>()).add("Kavita");
{Wagholi=[Priya, Arjun], Kharadi=[Kavita]}

computeIfAbsent returns the existing list, or creates one, stores it and returns that. The .add(...) then works on whichever it was. Compare with the version you would otherwise write:

List<String> list = byArea.get("Wagholi");
if (list == null) {
    list = new ArrayList<>();
    byArea.put("Wagholi", list);
}
list.add("Priya");

Five lines, one of which is the bug when somebody forgets the put.

Iteration order, and the three implementations

for (String k : new String[]{"Wagholi", "Kharadi", "Bavdhan", "Aundh"}) {
    hash.put(k, 1); linked.put(k, 1); tree.put(k, 1);
}
HashMap       : [Wagholi, Aundh, Bavdhan, Kharadi]
LinkedHashMap : [Wagholi, Kharadi, Bavdhan, Aundh]
TreeMap       : [Aundh, Bavdhan, Kharadi, Wagholi]

HashMap order is neither insertion nor sorted — it is an accident of hashing. It is stable for a given set of keys on a given Java version, which is exactly enough stability to let a test pass and a production report come out wrong later.

Never rely on HashMap order. If you need insertion order, say so with LinkedHashMap. If you need sorted order, TreeMap.

HashMap LinkedHashMap TreeMap
Order None Insertion Sorted by key
Lookup Fastest Fast Slower — a tree walk
Null keys One allowed One allowed Not allowed
Needs equals + hashCode Same Comparable or a Comparator
Extra Access-order mode for caches firstKey, headMap, subMap

TreeMap earns its place when you need ranges — every delivery between two dates, every amount above a threshold — because subMap, headMap and tailMap give them directly.

Nulls

Map<String, String> h = new HashMap<>();
h.put(null, "null key is allowed");
h.put("k", null);
System.out.println(h);
{null=null key is allowed, k=null}

HashMap permits one null key and any number of null values. Map.of rejects both with a NullPointerException, and TreeMap rejects null keys because it must compare them.

This creates a genuine ambiguity: map.get(k) returning null means either "no such key" or "the key is there with a null value". containsKey is the only way to tell them apart. Avoid storing nulls as values — it is nearly always better to leave the key out.

The trap: mutating a key after insertion

Map<List<String>, String> byKey = new HashMap<>();
List<String> key = new ArrayList<>(List.of("Wagholi"));
byKey.put(key, "route 1");

System.out.println(byKey.get(key));
key.add("Kesnand");
System.out.println(byKey.get(key));
System.out.println(byKey.size() + " " + byKey);
route 1
null
1 {[Wagholi, Kesnand]=route 1}

The entry is still in the map. Printing the map shows it. And the map cannot find it — with the very same object you used as the key.

A HashMap files entries by hashCode() at insertion time. Mutating the key changes its hash, so the lookup goes to the wrong bucket. The entry is stranded: unreachable by get, invisible to containsKey, still counted by size, still holding memory.

Map keys must be immutable, or at least never mutated while in use. Use String, a boxed number, an enum, or a record whose components are themselves immutable. This is the same failure the next lesson covers from the equals and hashCode side, and it is why records make such good keys.

Check your work

What does get return for a missing key? null — which throws NullPointerException the moment you assign it to an int. Use getOrDefault.

What does put return? The previous value for that key, or null if there was none.

Write the one-line counter. map.merge(key, 1, Integer::sum).

What does computeIfAbsent return? The existing value, or the newly created one after storing it — so .add(...) can be chained onto it directly.

What order does HashMap iterate in? None you may rely on. Not insertion, not sorted. Use LinkedHashMap or TreeMap if order matters.

Which map types reject null keys? TreeMap, because it must compare keys, and Map.of, which rejects null keys and values.

Why did get(key) return null for a key still visibly in the map? The key was mutated after insertion, changing its hashCode, so the lookup searches the wrong bucket. The entry is stranded — counted by size, unreachable by get.

What makes a good map key? Something immutable: String, a boxed number, an enum, or a record with immutable components.

Practice 2, the four counters. All four produce {veg=3, jain=2}. The hand-written one is five lines and contains the null check; getOrDefault is three and does not; merge(p, 1, Integer::sum) is one and cannot be got wrong. Preferring the last is not about brevity — it is that there is no null to mishandle.

Practice 4, grouping. With computeIfAbsent it is one line per row:

for (String line : lines) {
    String[] parts = line.split(",", -1);
    byArea.computeIfAbsent(parts[1], k -> new ArrayList<>()).add(parts[0]);
}

Use a TreeMap for byArea if the areas should print alphabetically, and leave the inner lists as ArrayList so customers stay in file order. Choosing a different implementation for the outer and inner collection is the point.

Practice

  1. Exercise the method table. Build a map of five customers to tiffin counts and call get on a missing key, getOrDefault, putIfAbsent, remove and containsKey. Then assign map.get(missing) to an int and read the exception.

  2. Count four ways. Count plan types from an array using the hand-written version, getOrDefault and merge. Confirm all three agree, then count the lines.

  3. Prove the order problem. Put the same five keys into a HashMap, a LinkedHashMap and a TreeMap and print each keySet(). Then add a sixth key to the HashMap and print again — note that existing keys can move.

  4. Group with computeIfAbsent. From lines of "Priya,Wagholi", build a map of area to customer names. Print areas alphabetically with customers in file order.

  5. Strand an entry. Use a mutable list as a key, mutate it, then try to get it back with the same object. Print size() and the map itself. Then redo it with a record as the key and confirm the problem disappears.

  6. Harder — a monthly report. From lines of "2026-09-27,Priya,Wagholi,2", build: total tiffins per customer, total per area, the busiest day, and the list of customers who took nothing after the 15th. Choose the map type for each deliberately and write one sentence per choice. This is a real report, and every piece of it is a map.

Next: Set, and removing duplicates properly.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship