Challenge

Fix the report builder

20 minjunior16 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 Starter

Hints

  1. Hint 1

    There are two separate defects: one about performance, one about correctness. Find the correctness one first — it survives any rewrite.

  2. Hint 2

    Look at how the delimiter is handled. What does the method return for an empty list?

  3. Hint 3

    deleteCharAt(sb.length() - 1) on an empty builder throws. That is the bug.

  4. 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?