Chapter 7 of 18
07 — Maps
Word counting with Map, merge, and a streams groupingBy refactor.
Kata: WordCount.count(sentence) returns {word -> occurrences}.
New ideas: Map<String, Integer>, HashMap, merge, and a streams groupingBy refactor.
Step 1 — write the test first
@Test
void countsEachWordOnce() {
Map<String, Integer> counts = WordCount.count("the quick brown fox");
assertEquals(Map.of(
"the", 1, "quick", 1, "brown", 1, "fox", 1
), counts);
}
RED (WordCount missing), then minimal:
public static Map<String, Integer> count(String sentence) {
Map<String, Integer> counts = new HashMap<>();
for (String word : sentence.toLowerCase().split("\\s+")) {
counts.put(word, 1);
}
return counts;
}
GREEN for the first test. But look at the code — it’s a lie. Every word gets count 1,
even repeats. The test deliberately didn’t cover that. Time for the second test:
@Test
void countsRepeatedWords() {
Map<String, Integer> counts = WordCount.count("one two two three three three");
assertEquals(Map.of("one", 1, "two", 2, "three", 3), counts);
}
RED. Now put isn’t enough — you need “increment value seen before”:
counts.merge(word, 1, Integer::sum);
Map.merge(key, value, remapFn) is the idiomatic Java “get-or-put-and-update” one-liner.
GREEN.
Step 2 — Map.of your test fixtures
Map.of("the", 1, "quick", 1) builds an immutable map literal — no hand-built fixtures.
Test expectations read as plain data.
REFACTOR — streams go both ways
Chapters 04 and 08 show loops → streams. Here’s the loop-free version, driven by the same tests:
public static Map<String, Long> countWithStreams(String sentence) {
return Arrays.stream(sentence.toLowerCase().split("\\s+"))
.filter(word -> !word.isBlank())
.collect(Collectors.groupingBy(word -> word, Collectors.counting()));
}
The hand-written merge loop becomes groupingBy(word, counting()). Two implementations, one
contract — your test proves them equal because the test is written against the behaviour, not
the algorithm.
Java-specific notes
- Generics recap:
Map<String, Integer>— keys and values typed;new HashMap<>()builds it. - Use
new HashMap<>()for building; the interface type (Map<...>) is what you expose. That’s the interface-first habit from chapter 06 again. split("\\s+")breaks on one-or-more whitespace; empty string yields[""]— hence the!word.isBlank()guard. Edge cases like this are exactly what extra tests should pin down.
Run it
gradle test --tests "maps.WordCountTest"
Key takeaway: merge and groupingBy are the two answers to “count things in Java” —
write the naive loop first, then refactor to the library call under green tests. Doing it in
that order is what builds fluency without cargo-cult.
Next: 08 — streams — the functional style done properly.