Chapter 5 — Reps
Conditioning, not grading. AI is OFF. The point of these reps is to build the hypothesis-test-fix muscle. Cheating here cheats only you — the project will not forgive a missing reflex.
For every rep that involves finding a bug, write the hypothesis down in a comment before you change a line of code. A hypothesis you didn’t write is a hypothesis that doesn’t exist.
Rep 1 — Read a Stack Trace Cold
Without running anything, read this stack trace:
Exception in thread "main" java.lang.NumberFormatException: For input string: "twelve"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:661)
at java.base/java.lang.Integer.parseInt(Integer.java:777)
at Calculator.add(Calculator.java:14)
at Main.main(Main.java:9)
Answer in a comment in a file Rep1.java:
- Which line of your code threw the exception? (Hint: the topmost frame that isn’t in
java.base.) - What is the type of failure?
- What was the offending input?
- Which line of code passed the bad input down?
Rep 2 — Three Crashes, Three Hypotheses
Create Rep2.java containing three small main-callable methods. Each crashes when called. Form a hypothesis for each before you fix it.
public class Rep2 {
public static void main(String[] args) {
// Uncomment one at a time:
// crash1();
// crash2();
// crash3();
}
static void crash1() {
String name = null;
System.out.println(name.length());
}
static void crash2() {
int[] nums = {1, 2, 3};
for (int i = 0; i <= nums.length; i++) {
System.out.println(nums[i]);
}
}
static void crash3() {
int x = 10;
int y = 0;
System.out.println(x / y);
}
}
For each crashN, before you fix it, write a one-line comment that names the exception and the cause. Then fix it minimally — preserve the spirit of the original code, do not delete the method.
Rep 3 — Off-by-One Without a Crash
Create Rep3.java:
public class Rep3 {
public static int sumOneToN(int n) {
int total = 0;
for (int i = 1; i < n; i++) {
total += i;
}
return total;
}
public static void main(String[] args) {
System.out.println(sumOneToN(5)); // expected 15
System.out.println(sumOneToN(10)); // expected 55
System.out.println(sumOneToN(1)); // expected 1
}
}
The output is wrong. The program does not crash. Find the bug using the hypothesis-test-fix discipline:
- Write down what each call returns and what it should return.
- Write down your hypothesis as a comment.
- Confirm with one
printlninside the loop. - Fix.
- Re-run and confirm all three cases.
Rep 4 — The == Trap Strikes Again
public class Rep4 {
public static String greet(String name) {
if (name == "Maya") return "Welcome back, Maya.";
return "Hello, " + name + ".";
}
public static void main(String[] args) {
String typed = new String("Maya");
System.out.println(greet(typed)); // expected "Welcome back, Maya."
}
}
Run it. Observe the wrong output. Diagnose: explain in a comment what == is comparing, and why it produces false here. Fix using .equals().
Rep 5 — Bisect a Long Method
Create Rep5.java containing a 40-line method processOrders that takes an int[] prices, computes a sequence of transformations, and returns a final total. Somewhere in the method, the wrong arithmetic happens. Use bisection: print the value of the running total halfway through, then quarter-way, etc., until you narrow the bug to one line.
Use this seed:
public class Rep5 {
public static int processOrders(int[] prices) {
int total = 0;
for (int p : prices) total += p; // sum
total = total * 2; // double it
total = total + 10; // tax
total = total - 5; // discount
total = total / 2; // split
total = total - prices.length; // BUG planted here — should be + not -
total = total * 1; // no-op
return total;
}
public static void main(String[] args) {
int[] prices = {10, 20, 30};
System.out.println(processOrders(prices)); // expected 67, got 61
}
}
Practice: insert two printlns — one at line “halfway” through, one at the quarter point. Decide which half contains the wrong transformation. Keep bisecting. Find the bug. Fix.
(Bonus: rewrite processOrders to break each transformation into its own well-named helper method. Notice how the bug becomes obvious when the math has names.)
Rep 6 — Print-Driven Investigation
public class Rep6 {
public static int[] doubleEvens(int[] input) {
int[] result = new int[input.length];
for (int i = 0; i < input.length; i++) {
if (input[i] % 2 == 0) {
result[i] = input[i] * 2;
}
}
return result;
}
public static void main(String[] args) {
int[] in = {1, 2, 3, 4, 5};
int[] out = doubleEvens(in);
for (int v : out) System.out.print(v + " ");
// Expected: 1 4 3 8 5
// Observed: 0 4 0 8 0
}
}
The odd numbers come out as 0. Hypothesize why before you change a line. Confirm with one print. Fix.
Rep 7 — Reproduce, Then Shrink
Create Rep7.java:
import java.util.ArrayList;
public class Rep7 {
public static int firstNegative(ArrayList<Integer> nums) {
for (int i = 1; i < nums.size(); i++) {
if (nums.get(i) < 0) return i;
}
return -1;
}
public static void main(String[] args) {
ArrayList<Integer> xs = new ArrayList<>();
for (int i : new int[]{ 5, 3, 1, -1, 4, 2 }) xs.add(i);
System.out.println(firstNegative(xs)); // expected 3, got 3 (PASSES)
ArrayList<Integer> ys = new ArrayList<>();
for (int i : new int[]{ -7, 4, 2 }) ys.add(i);
System.out.println(firstNegative(ys)); // expected 0, got ???
}
}
The first call passes. The second call fails. Shrink the failing input — what is the smallest list that still produces the wrong answer? Use the shrunk case to diagnose. Fix the off-by-one.
Rep 8 — Rubber Duck Rep
Write a short method gradeLetter(int score) that returns “A” for 90–100, “B” for 80–89, “C” for 70–79, “D” for 60–69, “F” below 60. Put a planted off-by-one in the boundary conditions yourself (use > where you meant >= or vice versa). Now: explain the method line by line, out loud, to a real or imagined duck. Listen for the moment your own voice catches on the bug.
If you don’t catch it, ask a classmate to listen while you explain. Same effect.
Rep 9 — Read the Helpful NPE Message
Compile and run:
public class Rep9 {
public static void main(String[] args) {
String first = null;
String second = "world";
String result = first.toUpperCase() + " " + second.toUpperCase();
System.out.println(result);
}
}
Note the exception message. Identify, from the message alone:
- Which variable was null.
- Which method was being called on it.
- The line number.
This is what Java 17’s “helpful NullPointerExceptions” buy you. Confirm by reading the message slowly.
Rep 10 — Confirm the Fix With a Regression Test
Take Rep 3’s fixed sumOneToN. Now add a tiny JUnit-style test class (or just an assertion in main if you don’t have JUnit configured yet):
public class Rep10 {
public static void main(String[] args) {
check(15, Rep3.sumOneToN(5), "sumOneToN(5)");
check(55, Rep3.sumOneToN(10), "sumOneToN(10)");
check(1, Rep3.sumOneToN(1), "sumOneToN(1)");
check(0, Rep3.sumOneToN(0), "sumOneToN(0)"); // edge case
}
static void check(int expected, int actual, String label) {
if (expected != actual) {
System.out.println("FAIL " + label + ": expected " + expected + ", got " + actual);
} else {
System.out.println("PASS " + label);
}
}
}
Now go back and re-introduce the original bug (change <= back to <). Run. Confirm the regression test catches it.
That’s what a regression test buys you: a tripwire that fires the next time someone breaks the same thing.
Rep 11 — Break It On Purpose
Six common bug patterns. Introduce each into a small program of your own choosing, run, observe the failure mode, and fix.
- Compare two
Strings with==. (Watch the answer be wrong.) - Use
<=in aforloop bound where<was correct. (Watch the array out-of-bounds.) - Forget to initialize a local variable before reading it. (Watch the compile error — Java forces you here, which is a kindness.)
- Divide an
intby anintand expect adoubleresult. (Watch the integer truncation.) - Iterate a
Listwith a for-each and call.remove()on it inside the loop. (Watch theConcurrentModificationException.) - Add one too few
}characters at the end of a file. (Watch the compiler complain.)
For each: write the hypothesis before you read the error, then confirm.
Done? One Last Thing.
Take the sabotaged file you’ll receive in Project 5. Don’t open it yet. First, with no source in front of you, write a one-page plan: “Here is how I will approach a sabotaged program in five runs or fewer.” Be specific. Write the five runs you intend to make and what you’ll look for on each one.
Then open the file and execute the plan. Notice where the plan held up and where reality demanded a different probe. That noticing is the meta-skill — debugging your debugging strategy.
Up next: Project 5 — Project 5: Sabotage Recovery.