ExerciseChallenge
Challenge
Replace five loops with collectors
25 minintermediate2–10 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- The downstream collector is what makes groupingBy useful
- collectingAndThen unwraps the Optional that maxBy leaves behind
- groupingBy nests, so a two-level index is one pass
- partitioningBy always returns both keys; groupingBy does not
- A collector version is not automatically better — one of these is worse
Starter
Starter.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
/**
* Challenge: five loops. Four of them are collectors waiting to be written.
*
* The fifth is not, and identifying it is worth as much as converting the
* other four. "Could this be a stream?" and "should this be a stream?" are
* different questions, and only one of them has an obvious answer.
*
* Each loop prints its result. Your collector version must print exactly the
* same thing.
*/
public class Starter {
record Employee(String name, String dept, String city, int salary) { }
static final List<Employee> STAFF = List.of(
new Employee("Ana", "eng", "pune", 120),
new Employee("Bo", "eng", "pune", 95),
new Employee("Cy", "eng", "kochi", 130),
new Employee("Di", "sales", "pune", 85),
new Employee("Ed", "sales", "kochi", 70),
new Employee("Fi", "ops", "kochi", 60));
public static void main(String[] args) {
// ── 1: headcount per department ──────────────────────────────────
Map<String, Integer> headcount = new TreeMap<>();
for (Employee e : STAFF) {
headcount.merge(e.dept(), 1, Integer::sum);
}
System.out.println("1 headcount : " + headcount);
// TODO 1: one groupingBy with a downstream collector.
// ── 2: names per department ──────────────────────────────────────
Map<String, List<String>> names = new TreeMap<>();
for (Employee e : STAFF) {
names.computeIfAbsent(e.dept(), k -> new ArrayList<>()).add(e.name());
}
System.out.println("2 names : " + names);
// TODO 2: groupingBy plus mapping. Note that computeIfAbsent in a loop
// IS groupingBy, written out by hand.
// ── 3: highest paid per department ───────────────────────────────
Map<String, String> topEarner = new TreeMap<>();
for (Employee e : STAFF) {
String current = topEarner.get(e.dept());
if (current == null || e.salary() > salaryOf(current)) {
topEarner.put(e.dept(), e.name());
}
}
System.out.println("3 top earner : " + topEarner);
// TODO 3: maxBy leaves an Optional inside the map. Which collector
// removes it before the group is finished?
// ── 4: department then city ──────────────────────────────────────
Map<String, Map<String, List<String>>> byDeptThenCity = new TreeMap<>();
for (Employee e : STAFF) {
byDeptThenCity
.computeIfAbsent(e.dept(), k -> new TreeMap<>())
.computeIfAbsent(e.city(), k -> new ArrayList<>())
.add(e.name());
}
System.out.println("4 dept/city : " + byDeptThenCity);
// TODO 4: the downstream collector can be another groupingBy. Use the
// three-argument form so the inner maps stay sorted.
// ── 5: the first employee earning under 80, or a default ─────────
String firstCheap = "none";
for (Employee e : STAFF) {
if (e.salary() < 80) {
firstCheap = e.name();
break;
}
}
System.out.println("5 first cheap : " + firstCheap);
// TODO 5: this one CAN be a stream. Write it, then decide whether it
// should be, and say what the loop communicates that the stream does
// not.
// TODO 6: every map printed above is a TreeMap on purpose. Say what
// would be wrong with printing the result of a plain groupingBy, and
// what the two ways to fix it are.
}
static int salaryOf(String name) {
return STAFF.stream().filter(e -> e.name().equals(name)).findFirst()
.map(Employee::salary).orElseThrow();
}
}Run it locally:
cd exercises/java/java8/collectors-and-grouping/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Every loop that builds a map with computeIfAbsent is a groupingBy.
Hint 2
maxBy returns an Optional. Nesting one inside a map is almost never what you want — which collector removes it as the group is finished?
Hint 3
For the two-level index, the downstream collector can be another groupingBy.
Hint 4
One of the five reads clearly as a loop and badly as a collector. Say which, and leave it alone.
Done when
- Four of the five are collector versions producing identical output
- No Optional appears in any resulting map type
- The one that should stay a loop is identified, with the reason
- Every printed map is sorted or uses a map factory, never HashMap order
← Back to How do Collectors.groupingBy and toMap differ in failure modes?