Chapter 3 — Reps
Conditioning, not grading. Exception-handling reps.
Ground rules:
- Type every line yourself. No copy-paste of try/catch templates.
- Compile and run after every rep. Watch what happens at runtime.
- AI stays OFF. Phase 1. Exception handling is one of the rep-heaviest topics in Java; the muscle has to live in your hands.
- When the program crashes, read the stack trace top to bottom. That’s how you’ll find every bug for the rest of your career.
Working in OnlineGDB Java 17 or your local JDK. Either path; don’t switch mid-rep.
Reps 1–3: try / catch / finally
Rep 1 — Forced NumberFormatException
Create ParseAge.java:
import java.util.Scanner;
public class ParseAge {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Enter your age: ");
String input = in.nextLine();
try {
int age = Integer.parseInt(input);
System.out.println("Next year you will be " + (age + 1) + ".");
} catch (NumberFormatException e) {
System.out.println("That wasn't a number. Got: " + input);
} finally {
in.close();
System.out.println("Goodbye.");
}
}
}
Run it three times:
- Type
25. Confirm normal path. - Type
seven. Confirm catch block runs. - Type nothing (just hit enter). Confirm catch block runs (empty string also fails to parse).
In all three cases, “Goodbye.” prints. That’s the finally block doing its job.
Rep 2 — Multi-catch
Modify Rep 1 so that the try block also reads a second integer that gets divided into the first. Catch both NumberFormatException and ArithmeticException in a single multi-catch:
try {
int a = Integer.parseInt(in.nextLine());
int b = Integer.parseInt(in.nextLine());
System.out.println(a + " / " + b + " = " + (a / b));
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("Bad input: " + e.getClass().getSimpleName() + " — " + e.getMessage());
}
Test with 10 and 2 (normal), ten and 2 (NumberFormatException), 10 and 0 (ArithmeticException). Notice how the catch block tells you which kind it was.
Rep 3 — Try-with-resources
Rewrite Rep 1 to use try-with-resources for the Scanner. The structure becomes:
public class ParseAge {
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
System.out.print("Enter your age: ");
String input = in.nextLine();
try {
int age = Integer.parseInt(input);
System.out.println("Next year you will be " + (age + 1) + ".");
} catch (NumberFormatException e) {
System.out.println("That wasn't a number. Got: " + input);
}
}
System.out.println("Goodbye.");
}
}
The Scanner closes automatically when the outer try block exits — no finally, no explicit close(). This is the modern idiom. Use it for the rest of the course unless you have a specific reason not to.
Reps 4–5: Throwing and Custom Exceptions
Rep 4 — Throw with a useful message
Write a class BankAccount with one method:
public void withdraw(double amount) {
// your code here
}
The method must:
- Throw
IllegalArgumentExceptionifamountis negative. - Throw
IllegalStateException(with a useful message including the current balance and the attempted amount) ifamountexceeds the balance. - Subtract
amountfrom the balance otherwise.
Write a small main that calls withdraw three times — once normally, once with a negative amount, once with too much — wrapped in try/catch blocks that print the exception’s class and message. Confirm the diagnostics are useful.
Rep 5 — Custom Checked Exception with Chaining
Write a custom exception:
public class CatechismLoadException extends Exception {
public CatechismLoadException(String message) {
super(message);
}
public CatechismLoadException(String message, Throwable cause) {
super(message, cause);
}
}
Then write a method:
public static String loadCatechism(String path) throws CatechismLoadException {
try {
return java.nio.file.Files.readString(java.nio.file.Path.of(path));
} catch (java.io.IOException e) {
throw new CatechismLoadException("Could not load catechism from " + path, e);
}
}
In main, call loadCatechism("does-not-exist.txt") inside a try/catch. In the catch block, print:
System.out.println("Top-level message: " + e.getMessage());
System.out.println("Cause: " + e.getCause());
Notice that the cause is the original IOException (specifically NoSuchFileException). The chain is preserved.
Reps 6–7: Fail Fast
Rep 6 — Objects.requireNonNull
Write a Person class:
import java.util.Objects;
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = Objects.requireNonNull(name, "name");
if (age < 0) throw new IllegalArgumentException("age must be non-negative; got " + age);
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
Write a main that tries to construct:
new Person("Maya", 21)— normal.new Person(null, 21)— should throwNullPointerException("name").new Person("Maya", -1)— should throwIllegalArgumentExceptionwith a useful message.
Confirm each diagnostic is precise. Notice how requireNonNull produces a clear message instead of the dreaded NullPointerException with no context.
Rep 7 — Refactor a Brittle Method
Below is a method that crashes ungracefully on bad input. Refactor it to fail fast with clear, specific exceptions.
public static int average(int[] scores, int start, int end) {
int total = 0;
for (int i = start; i < end; i++) {
total += scores[i];
}
return total / (end - start);
}
Your refactor should:
- Throw
NullPointerException("scores")ifscoresis null. - Throw
IllegalArgumentExceptionifstart > end(with a message that includes both values). - Throw
IndexOutOfBoundsExceptionifstart < 0orend > scores.length. - Throw
IllegalArgumentExceptionifstart == end(empty range — division by zero would otherwise happen).
Write a Javadoc above the method documenting all four @throws. Compile and test each failure path.
Reps 8–9: Graceful Degradation and Logging
Rep 8 — Graceful Input Loop
Write a program that asks the user for a positive integer and keeps asking until they give a valid one. Use try/catch to handle NumberFormatException and a range check to handle non-positive values. Don’t crash. Don’t accept bad input.
import java.util.Scanner;
public class PositiveIntPrompt {
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
int n = -1;
while (n <= 0) {
System.out.print("Enter a positive integer: ");
String input = in.nextLine();
try {
n = Integer.parseInt(input);
if (n <= 0) {
System.out.println("Must be positive; try again.");
}
} catch (NumberFormatException e) {
System.out.println("Not a number; try again.");
}
}
System.out.println("Thanks. You entered " + n);
}
}
}
Test with seven, -3, 0, then finally 42. Confirm the program never crashes and only accepts the valid input.
This is “graceful degradation at the edge” — exactly the pattern Project 3 will ask you to apply to user input.
Rep 9 — Logging with java.util.logging
Write this:
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileLoader {
private static final Logger LOG = Logger.getLogger(FileLoader.class.getName());
public static String load(String path) {
LOG.info("Attempting to load " + path);
try {
String content = Files.readString(Path.of(path));
LOG.info("Loaded " + content.length() + " characters from " + path);
return content;
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load " + path, e);
return "";
}
}
public static void main(String[] args) {
String good = load("Hello.java"); // assuming you have one in scratch
String bad = load("does-not-exist.txt");
System.out.println("good length: " + good.length());
System.out.println("bad length: " + bad.length());
}
}
Run it. Notice that JUL’s default output format includes the timestamp, the class name, the log level, and (for Level.WARNING with a throwable) the full stack trace. Project 3 Hard will lean on this same pattern.
If you don’t have a Hello.java next to your code, swap in any file you know exists. The bad path should still produce the warning.
Rep 10 — Break a Working Program
Take Rep 6’s Person class. Try the following modifications, one at a time, and observe:
-
Change
Objects.requireNonNull(name, "name")tothis.name = name;. Now constructnew Person(null, 21). What exception do you see? How does the diagnostic differ from therequireNonNullversion? -
Remove the
if (age < 0)check. Constructnew Person("Maya", -5). Notice that the program accepts the bad data and the bug shows up later, whengetAge()returns -5 and someone uses it. -
Add an empty
catch (Exception e) { }around the constructor in themain. Trynew Person(null, 21). Notice the program silently continues with a half-constructed object. Never do this in real code. The point of this rep is to feel what silent catch-and-ignore costs.
Restore the correct version. Notice how much more diagnostic value the strict version gives you.
Rep 11 — Read a Stack Trace
Run this on purpose:
public class Crash {
public static void main(String[] args) {
String s = null;
System.out.println(s.length());
}
}
You’ll get something like:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at Crash.main(Crash.java:4)
Notice everything the trace tells you:
- The exception type (
NullPointerException). - A precise message (
Cannot invoke "String.length()" because "s" is null). - The exact file and line (
Crash.java:4). - The method (
Crash.main).
Java’s modern stack traces (Java 14+) tell you which variable was null. Older Java just said NullPointerException with no message — which is one of the reasons Java 17 LTS is a big improvement. Be grateful for it.
Now intentionally produce three more crashes (an ArrayIndexOutOfBoundsException, a NumberFormatException, and a custom one you throw yourself) and read each stack trace top to bottom. Read every one. The trace is the diagnostic.
Done? One Last Thing.
From scratch, no looking — write a program that:
- Defines a custom checked exception
BadVerseException. - Has a method
parseVerse(String input)that splits a Bible verse reference like"Romans 8:28"into a book name, chapter, and verse number — throwingBadVerseException(with a useful message and the originalNumberFormatExceptionchained as the cause) if the input is malformed. - Has a
mainthat tries three inputs: a valid one, a malformed one, andnull(which should fail withNullPointerExceptionfromrequireNonNull). - Catches every exception and prints both the message and the cause.
If you can write that cold, you have the move.
Up next: Project 3 — Project 3: The Robust Refactor.