ExerciseChallenge
Challenge
Fix the report builder
20 minjunior1–6 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Concatenation inside a loop is the case that matters
- toString() copies, so the result stops tracking the buffer
- StringBuilder has no equals() override — comparing two builders is identity
- String.join and StringJoiner remove the trailing-delimiter bug entirely
- Pre-sizing skips resizes when the final length is known
Starter
Starter.java
import java.util.*;
/**
* Challenge: this report builder has two defects — one performance, one
* correctness. The correctness one is the more serious, and it survives any
* amount of performance tuning.
*
* Find both before you change anything.
*/
public class Starter {
record Row(String name, int count) {}
/** Defect 1: concatenation in a loop. */
static String buildSlow(List<Row> rows) {
String out = "";
for (Row r : rows) {
out += r.name() + "=" + r.count() + ", ";
}
// Defect 2 lives on the next line.
return out.substring(0, out.length() - 2);
}
/** The same shape with a builder — and the same second defect. */
static String buildWithBuilder(List<Row> rows) {
StringBuilder sb = new StringBuilder();
for (Row r : rows) {
sb.append(r.name()).append('=').append(r.count()).append(", ");
}
sb.deleteCharAt(sb.length() - 1);
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
public static void main(String[] args) {
List<Row> rows = List.of(
new Row("hashmap", 12),
new Row("arraylist", 7),
new Row("strings", 21));
System.out.println("slow : " + buildSlow(rows));
System.out.println("builder : " + buildWithBuilder(rows));
// Now the case nobody tested.
List<Row> empty = List.of();
try {
System.out.println("empty (slow) : " + buildSlow(empty));
} catch (Exception e) {
System.out.println("empty (slow) -> " + e.getClass().getSimpleName());
}
try {
System.out.println("empty (builder) : " + buildWithBuilder(empty));
} catch (Exception e) {
System.out.println("empty (builder) -> " + e.getClass().getSimpleName());
}
// TODO 1: name both defects in a comment. Which one would page you at
// 3am, and which one would just show up on a dashboard?
// TODO 2: write buildFixed(List<Row>) that is linear, correct on an
// empty list, and does not delete a trailing anything.
//
// Use String.join, StringJoiner, or Collectors.joining — pick one and
// say why. The manual append-then-delete pattern is what you are
// replacing, so do not reimplement it.
// TODO 3: predict this, then run it:
//
// StringBuilder a = new StringBuilder("x");
// StringBuilder b = new StringBuilder("x");
// System.out.println(a.equals(b));
//
// Explain the result, and what it means for using a StringBuilder as a
// HashMap key or in a Set.
// TODO 4: predict this too:
//
// StringBuilder sb = new StringBuilder("abc");
// String s = sb.toString();
// sb.append("def");
// System.out.println(s);
//
// What does toString() actually give you?
}
}Run it locally:
cd exercises/java/strings/string-vs-stringbuilder/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
There are two separate defects: one about performance, one about correctness. Find the correctness one first — it survives any rewrite.
Hint 2
Look at how the delimiter is handled. What does the method return for an empty list?
Hint 3
deleteCharAt(sb.length() - 1) on an empty builder throws. That is the bug.
Hint 4
Java 8 added something that handles separator-between-not-after for you. Two things, actually.
Done when
- The report builds in linear time
- An empty input list returns an empty report rather than throwing
- No trailing delimiter, and no manual deletion of one
- A comment says which Java 8 API you would use and why it is better than fixing the loop
← Back to What is the difference between String, StringBuilder and StringBuffer?