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
-
Exercise the method table. Build a map of five customers to tiffin counts and call
geton a missing key,getOrDefault,putIfAbsent,removeandcontainsKey. Then assignmap.get(missing)to anintand read the exception. -
Count four ways. Count plan types from an array using the hand-written version,
getOrDefaultandmerge. Confirm all three agree, then count the lines. -
Prove the order problem. Put the same five keys into a
HashMap, aLinkedHashMapand aTreeMapand print eachkeySet(). Then add a sixth key to theHashMapand print again — note that existing keys can move. -
Group with
computeIfAbsent. From lines of"Priya,Wagholi", build a map of area to customer names. Print areas alphabetically with customers in file order. -
Strand an entry. Use a mutable list as a key, mutate it, then try to
getit back with the same object. Printsize()and the map itself. Then redo it with a record as the key and confirm the problem disappears. -
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