Debugging and tools
How to find and prevent bugs: assertions, logging versus printing, the IDE debugger and breakpoints, reading stack traces, unit tests, building and running from the command line, and Java version compatibility.
Learning goals
- Use assertions and tests to catch bugs early
- Step through code with a debugger
- Read a stack trace from the bottom up
- Compile and run from the command line
Prerequisites
Assertions
assert condition : message; checks an assumption and throws AssertionError when it is false. Assertions are disabled by default; enable them with java -ea. Use them for internal invariants, never for validating user input.
Because they may be off, an assertion must not have side effects.
1static int pop(int[] stack, int size) {2assert size > 0 : "pop from empty stack";3return stack[size - 1];4}5// run: java -ea Main
Logging vs printing
Temporary prints are quick, but they clutter output and are easy to forget. java.util.logging or a logging library gives levels (INFO, WARNING, SEVERE), timestamps and switches. In contests print debug lines to System.err and remove them before submitting.
Never log inside a hot loop; building the message costs time even if it is discarded.
1Logger log = Logger.getLogger("algo");2log.info("start");3System.err.println("i=" + 3 + " sum=" + 12); // debugging without touching stdout
IDE debugger
A debugger pauses the program and lets you inspect variables, step line by line and evaluate expressions. In VS Code or IntelliJ start a run in debug mode, then use Step Over (next line), Step Into (enter a call), Step Out (finish the method) and Continue (run to the next breakpoint).
Watching a variable change step by step is the same skill as a tracing table, done by the machine.
Breakpoints
A breakpoint stops execution at a line. A conditional breakpoint stops only when an expression is true (i == 9999), a hit-count breakpoint after N passes, and an exception breakpoint when a given exception is thrown. These let you skip thousands of uninteresting iterations.
Place the breakpoint just before the line you suspect, not at the start of the program.
Stack traces
A stack trace lists the call chain at the moment of an exception: the first line is the exception and message, the next is where it was thrown, then each caller. Read the top first, find the first line in your own code, and look at the values used on that line.
Typical messages: ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5 (off by one), NullPointerException (unset reference), StackOverflowError (missing base case).
1Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 52at Main.sum(Main.java:6) <- line to inspect first3at Main.main(Main.java:12) <- who called it
Unit tests
A unit test runs a small piece of code on known input and checks the result automatically. With JUnit 5, annotate methods with @Test and use assertEquals. Test the normal case, the edge cases and a regression for every bug you fix.
For algorithms, also compare against a brute-force version on random small inputs.
1import org.junit.jupiter.api.Test;2import static org.junit.jupiter.api.Assertions.*;34class SearchTest {5@Test void findsMiddle() { assertEquals(2, Search.binarySearch(new int[]{1, 3, 5, 7}, 5)); }6@Test void missingReturnsMinusOne() { assertEquals(-1, Search.binarySearch(new int[]{1, 3}, 2)); }7@Test void emptyArray() { assertEquals(-1, Search.binarySearch(new int[0], 1)); }8}
Build and run
Compile with javac Main.java and run with java Main. Since Java 11 you can run a single source file directly with java Main.java. Use -ea to enable assertions, -Xmx512m to cap the heap and -Xss to enlarge the thread stack for deep recursion.
Build tools (Maven, Gradle) automate compiling, testing and packaging for larger projects.
1javac Main.java2java -ea Main < input.txt3java Main.java # compile and run in one step (Java 11+)4java -Xss64m -Xmx256m Main # larger stack, bounded heap
Java version compatibility
Check the Java version with java -version. Language features arrive over time: var (10), switch expressions (14), text blocks (15), records (16), sealed classes (17) and pattern matching for instanceof (16). Code that uses a newer feature will not compile on an older JDK.
This course targets Java 17. If a judge or project uses an older version, avoid records and switch expressions, or check what the platform supports first.
| Feature | Java version |
|---|---|
| var for locals | 10 |
| Switch expressions | 14 |
| Text blocks | 15 |
| Records, pattern instanceof | 16 |
| Sealed classes | 17 |
| Long-term support releases | 8, 11, 17, 21 |
Practice ladder
Now try it yourself
Easy
- Add an assertion to a method that expects a non-negative argument
- Run a program with and without -ea
Medium
- Read a stack trace for an off-by-one error and identify the faulty line
- Write three JUnit tests for a max method, including an empty array
Advanced
- Use a conditional breakpoint to find the first iteration where a running sum becomes negative
Key takeaways
- Assertions and tests catch bugs close to their cause.
- Read stack traces from the top, starting with your own code.
- Know your JDK version; this course uses Java 17.