Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

This book supports a 50-minute, hands-on Java debugging workshop for students who are still building debugging confidence.

You will use a small multi-file Java program and debug it in progressive levels. Each level focuses on practical workflow: run, observe, investigate, fix, re-run.

By the end of the workshop, students should be able to:

  • Distinguish compile-time, runtime, and logic bugs.
  • Read exception messages and stack traces to find root causes.
  • Use breakpoints, step controls, variable inspection, and call stack navigation.
  • Debug across multiple files and method calls.
  • Fix one bug while checking that other behavior still works.

Workshop flow:

  1. Start Guide: set up OnlineGDB and debugging controls.
  2. Workshop Overview: timing for each section.
  3. Level 1–4: progressive debugging exercises.
  4. Solutions: standby reference for instructor/students.

Starter source files for copy/paste live under code/levels-1-2/ (Levels 1–2) and code/levels-3-4/ (Levels 3–4). Each folder is its own OnlineGDB project; both use Main.java as the entry point.

This is designed to be active. Students should spend most of the session debugging code, not just reading.

Start Guide (≈ 5-10 minutes)

This workshop uses OnlineGDB - a free, modern, browser-based IDE that supports multiple Java files and a Java debugger with no paywall.

You can follow these steps on any computer with a web browser.


1. Open the IDE

  1. Open your browser and go to https://www.onlinegdb.com/online_java_compiler.
  2. You should see a Java file named Main.java in the editor and a Run and Debug button near the top.

Instructor note (1 min): Make sure everyone is on the site and sees the Java editor.


2. Create the project layout

For Project A (Levels 1-2), create only these files:

  1. Click New File.
  2. Name the file exactly: CalculatorExercises.java.
  3. Repeat and create these files as well:
    • LoopExercises.java
  4. Keep the default Main.java file for now – we will replace its contents later.

OnlineGDB saves these files in the temporary project in your browser.

For Project B (Levels 3-4), create a new OnlineGDB Java project. OnlineGDB expects the program entry point in a file named Main.java with public class Main.

Add these files:

  • Main.java
  • Player.java
  • ScoreUtils.java
  • LevelLoader.java
  • InputParser.java

3. How to paste in the starter code

Each exercise chapter in this workshop includes copy‑paste‑ready code blocks.

For each file:

  1. Click the file tab at the top (for example CalculatorExercises.java).
  2. Select all existing code in the editor and delete it.
  3. Copy the corresponding code block from the workshop and paste it into the editor.
  4. Press Ctrl+S (or use the Save icon) to save that file.

Tip: Double‑check the file name at the top of the editor matches the heading above the code block in the book.


4. Running the program

OnlineGDB runs the file that contains public static void main(String[] args). In this workshop:

  • Project A (Levels 1-2) uses Main.java.
  • Project B (Levels 3-4) also uses Main.java (in a separate OnlineGDB project).

To run:

  1. Make sure the correct main file (for example Main.java) is open in the editor.
  2. Click the Run button at the top.
  3. Watch the Console panel at the bottom for:
    • Program output
    • Error messages
    • Stack traces (when exceptions occur)

If the program doesn’t compile, error messages will appear in the console with line numbers.


5. Starting a debugging session

To start the debugger in OnlineGDB:

  1. Open the file that contains main in your current project.
  2. Click in the gutter (the narrow column to the left of the line numbers) next to the line where you want execution to pause.
    • A red dot appears - this is a breakpoint.
  3. Click the Debug button at the top.
  4. Then click the green Start button to begin execution.
  5. When execution reaches a breakpoint, the debugger pauses at that point.

6. Stepping through code

When the program is paused on a breakpoint, use the debugger controls near the console area (not above the editor, in OnlineGDB):

  • Step Over: runs the current line and stops at the next line in the same method.
    • Use this when you do not need to go inside a method call.
  • Step Into: jumps into the method being called on the current line.
    • Use this to enter helper methods in other files/classes.
  • Step Out: runs the rest of the current method and returns to the caller.
    • Use this to quickly leave a method once you understand it.

You can also press Continue to run until the next breakpoint or program exit.


7. Inspecting variables and call stack

When stopped on a breakpoint:

  • Variables / Locals panel:
    • Shows the values of local variables in the current method.
    • Expands objects so you can see their fields (for example, player.name, player.score).

Arrays in OnlineGDB: the debugger often does not show array elements or array.length in a useful way. When you need to inspect a length or a derived value during debugging, assign it to a local int (for example int scoreLength = scores == null ? 0 : scores.length;) and watch that variable. This workshop’s starter code uses that pattern where it matters.

  • Call stack:
    • Shows the chain of method calls that led to the current line.
    • Clicking on a stack frame jumps to that frame’s source (often in another file).
  • Console:
    • Shows printed output (System.out.println) and error messages.
    • Useful for comparing print debugging with debugger‑based debugging.

Note on watch expressions:

  • In OnlineGDB, watch behavior is limited.
  • In IDEs like IntelliJ IDEA or VS Code, watch expressions are more reliable for tracking values such as scores.length or i.

8. If something goes wrong

If the debugger seems stuck or weird:

  1. Click Stop to end the current run.
  2. Re‑click Debug.
  3. Click the green Start button again.
  4. Make sure your breakpoints are on executable lines (not on blank lines or comments).

If the code doesn’t look like this book:

  1. Re‑copy the code from the chapter into the right file.
  2. Confirm the file name and class name match exactly.

Once everyone can set a breakpoint and step a line in Main.java, you are ready for Level 1.

Workshop Overview

Total time goal: ≈ 50 minutes

  • Start Guide – 5 minutes (already completed)
  • Level 1 – Reading Errors – 10 minutes
  • Level 2 – Debugger Basics – 12 minutes
  • Level 3 – Multi‑File Debugging – 13 minutes
  • Level 4 – Layered Bugs – 10 minutes (stretch / as time allows)

Level 1 – Reading Errors (≈ 10 minutes)

Concepts: compile‑time vs runtime errors, error messages, stack traces, print debugging vs debugger.

We will start with two small files:

  • CalculatorExercises.java
  • LoopExercises.java

1. Setup the files

Create or open these files in OnlineGDB and paste in the code below.

CalculatorExercises.java

public class CalculatorExercises {
    public static int divide(int a, int b) {
        return a / b;
    }

    public static int sumFirstThree(int[] numbers) {
        int numberCount = (numbers == null) ? 0 : numbers.length;
        int total = 0;
        for (int i = 0; i <= 3; i++) {
            total += numbers[i];
        }
        return total;
    }
}

LoopExercises.java

public class LoopExercises {
    public static int countUpTo(int n) {
        int count = 0;
        for (int i = 1; i < n; i++) {
            count++;
        }
        return count;
    }
}

Main.java

Replace the contents of Main.java with:

public class Main {
    public static void main(String[] args) {
        // Example 1: division
        System.out.println("Trying division...");
        int result = CalculatorExercises.divide(10, 0);
        System.out.println("Result: " + result);

        // Example 2: sum first three
        int[] nums = {1, 2};
        System.out.println("Trying sumFirstThree...");
        int sum = CalculatorExercises.sumFirstThree(nums);
        System.out.println("Sum: " + sum);

        // Example 3: loop
        System.out.println("Trying countUpTo(5)...");
        int count = LoopExercises.countUpTo(5);
        System.out.println("Count: " + count);
    }
}

2. Run and observe

  1. Open Main.java.
  2. Click Run (not Debug yet).
  3. Watch the Console output and error messages.

Expected symptoms:

  • The program should throw a runtime error (an exception) on the division line.
  • You should see a stack trace in the console mentioning:
    • The type of exception.
    • The line number in CalculatorExercises.java.
    • The line number in Main.java where it was called.

If you fix that and run again, you will later see:

  • Another exception from sumFirstThree.
  • A logic bug: countUpTo(5) prints the wrong count but does not crash.

3. Read the error messages

Focus on the first failure (division by zero):

  1. Look for the exception type (e.g., java.lang.ArithmeticException).
  2. Find the message (e.g., / by zero).
  3. Find the topmost line from your code in the stack trace:
    • It should show CalculatorExercises.divide with a line number.
    • It should show Main.main with a line number.

Answer (to yourself or a neighbor):

  • Is this a compile‑time error or a runtime error? Why?
  • Which line in which file is the “root cause” line?

4. Quick detective work (no debugger yet)

Without changing anything, ask:

  • Why might b be zero in divide?
  • Why might sumFirstThree hit an out‑of‑bounds index?

Use print debugging:

  1. Add System.out.println("a=" + a + ", b=" + b); at the start of divide.
  2. Add System.out.println("numbers length=" + numbers.length); at the start of sumFirstThree.
  3. Run again and see the printed values before the crash.

5. Fix the obvious runtime bugs

Make the program stop crashing, but don’t worry about perfect logic yet.

Suggested changes (try on your own first):

  • In divide, guard against b == 0:
    • Print an error and return 0, or
    • Throw a more descriptive exception.
  • In sumFirstThree, handle arrays with fewer than 3 elements.

Once you have fixed the crashes:

  1. Comment out or remove the temporary println calls you added.
  2. Run again and confirm there are no exceptions.

6. Find the logic bug

Now focus on LoopExercises.countUpTo(5):

  1. What do you expect countUpTo(5) to return?
  2. What does it actually print?
  3. Is this a compile‑time error, runtime error, or logic bug?

You can:

  • Add a println inside the loop to print i each time.
  • Or (in the next Level) use the debugger to step through it.

7. Wrap‑up questions (Level 1)

Discuss:

  • What is the difference between a compile‑time error and a runtime exception?
  • How does the stack trace help you find the root cause?
  • When is print debugging helpful, and when might it be slower than using a debugger?

8. Before Level 2

You will practice the same program again with the debugger in Level 2.

Before you start Level 2, comment out your Level 1 fixes in divide, sumFirstThree, and countUpTo (use // or /* */) so the buggy behavior returns. In Level 2 you will uncomment those fixes again after the debugger exercises so you do not repeat the same edits from scratch.

Level 2 – Debugger Basics (≈ 12 minutes)

Concepts: breakpoints, stepping over/into/out, variable inspection, tracking changing state, logic bugs.

We reuse the Level 1 project. You will debug with breakpoints and stepping, not only console output.


1. Bring the bugs back (without losing your fixes)

You should still have:

  • CalculatorExercises.java
  • LoopExercises.java
  • Main.java

If you have not already (see Level 1, section 8): comment out your Level 1 fixes in divide, sumFirstThree, and countUpTo so the original buggy lines run again. Keep the commented fix code in the file so you can uncomment it later instead of retyping.

Arrays in OnlineGDB: you usually cannot inspect array elements or lengths in the Variables panel. The starter sumFirstThree includes a local numberCount so you can see how many elements the array has while debugging.


2. Set your first breakpoint

  1. Open Main.java.
  2. Click in the gutter on the line:
    • int result = CalculatorExercises.divide(10, 0);
  3. You should see a red dot (breakpoint) next to that line.

Start the debugger:

  1. Click Debug.
  2. Click the green Start button.
  3. When execution pauses, the debugger is stopped at your breakpoint.

3. Step over, then Continue

When paused at the breakpoint:

  1. Look at the Variables/Locals panel for args and any locals.
  2. Click Step Over once on the divide line.
  3. Click Continue so the program runs forward. In OnlineGDB, stepping alone may not surface the failure you expect; Continue lets execution run until the exception or the next breakpoint.

If an exception appears:

  • Note the line where execution stops.
  • Compare to the stack trace you saw in Level 1.

Repeat with a breakpoint on return a / b; inside CalculatorExercises.divide:

  1. Click Debug, then the green Start button.
  2. Inspect a and b in the Variables panel right before the division.

4. Fix divide before the next exercise

Before you go on to section 5: restore your divide fix so dividing by zero no longer crashes (uncomment your Level 1 fix or re-apply it). Run with Run once to confirm the program reaches the sumFirstThree section.

If divide still throws, you will not reach the next breakpoint in main.


5. Step into sumFirstThree

Practice Step Into and Step Out with sumFirstThree:

  1. Set a breakpoint in Main.main on:
    • int sum = CalculatorExercises.sumFirstThree(nums);
  2. Click Debug, then the green Start button.
  3. When paused on this line, click Step Into.

You should now be inside CalculatorExercises.sumFirstThree.

While stepping through the loop:

  1. Watch numberCount, i, and total in the Variables panel (not numbers.length or array slots).
  2. Use Step Over to run each iteration.
  3. Notice when i is no longer a valid index.

In OnlineGDB, skip watch expressions and rely on Variables/Locals.

When you understand the failure:

  1. Click Stop to end the debugging session.
  2. Uncomment your Level 1 fix for sumFirstThree (or re-apply it) so short arrays do not crash and the first three elements are summed when present.

6. Logic bug: countUpTo

  1. Set a breakpoint in Main on:
    • int count = LoopExercises.countUpTo(5);
  2. Click Debug, then the green Start button.
  3. When paused, click Step Into to enter countUpTo.
  4. Use Step Over through the loop.

Observe:

  • How many times does the loop run?
  • What are the values of i and count at each step?

Then uncomment your Level 1 fix for countUpTo (or re-apply it) so the result matches what you expect.

Re-run with the debugger to confirm.


7. After the debugger exercises

Uncomment any remaining commented Level 1 fixes so divide, sumFirstThree, and countUpTo are all corrected and the program runs cleanly. You should not need to redo Level 1 from scratch.


8. Compare debugger vs print debugging

Briefly discuss or think about:

  • When was the debugger faster or clearer than System.out.println?
  • When might print debugging still be useful?
  • How did stepping into sumFirstThree and countUpTo help you see the bug more clearly?

You are now ready for Level 3 (a new OnlineGDB project with a different Main.java).

Level 3 – Multi‑File Debugging (≈ 13 minutes)

Concepts: stepping between files, multi‑file logic bugs, call stack, unexpected control flow, bad method arguments.

We will work with a tiny “scoreboard” app:

  • Main.java – entry point and helpers (printPlayerStats).
  • Player.java – represents a player and their scores.
  • ScoreUtils.java – helper methods.

OnlineGDB expects the class with main to be in Main.java with public class Main. Use a new OnlineGDB Java project for Levels 3–4 so you do not have two main methods in one project.


1. Setup the files

Create a new OnlineGDB Java project for Level 3. Add these files and paste in the code.

Player.java

public class Player {
    private String name;
    private int[] scores;

    public Player(String name, int[] scores) {
        this.name = name;
        this.scores = scores;
    }

    public String getName() {
        return name;
    }

    public int[] getScores() {
        return scores;
    }
}

ScoreUtils.java

public class ScoreUtils {

    public static double averageScore(int[] scores) {
        int scoreLength = (scores == null) ? 0 : scores.length;
        int total = 0;
        for (int i = 0; i < scoreLength; i++) {
            total += scores[i];
        }
        return (double) total / (scores.length + 1);
    }

    public static int bestScore(int[] scores) {
        int scoreLength = (scores == null) ? 0 : scores.length;
        int best = 0;
        for (int score : scores) {
            if (score > best) {
                best = score;
            }
        }
        return best;
    }

    public static int totalScore(Player player) {
        int[] arr = player.getScores();
        int scoreLength = (arr == null) ? 0 : arr.length;
        int sum = 0;
        for (int s : arr) {
            sum += s;
        }
        return sum;
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Player alice = new Player("Alice", new int[] {10, 15, 20});
        Player bob = new Player("Bob", new int[] {5, 7});
        Player charlie = new Player("Charlie", new int[] {});

        System.out.println("=== Scoreboard ===");

        printPlayerStats(alice);
        printPlayerStats(bob);
        printPlayerStats(charlie);

        Player mystery = null;
        System.out.println("\nMystery total score (should handle null):");
        int total = ScoreUtils.totalScore(mystery);
        System.out.println("Total: " + total);
    }

    private static void printPlayerStats(Player player) {
        int[] scores = player.getScores();
        int scoreLength = (scores == null) ? 0 : scores.length;

        double avg = ScoreUtils.averageScore(scores);
        int best = ScoreUtils.bestScore(scores);
        int lineTotal = ScoreUtils.totalScore(player);

        System.out.println("Player: " + player.getName());
        System.out.println("  Average: " + avg);
        System.out.println("  Best: " + best);
        System.out.println("  Total: " + lineTotal);
        System.out.println();
    }
}

Make sure:

  • All files are saved.
  • Only Main.java contains public static void main.

2. Run and observe

  1. Open Main.java.
  2. Click Run.

Expected symptoms:

  • The program may:
    • Print suspicious/incorrect averages.
    • Print wrong best scores (especially for negative or empty sets).
    • Crash with a NullPointerException when using the mystery player.

Focus on:

  • Which output looks wrong?
  • Where does the stack trace say the null pointer came from?

3. Step across files with the debugger

Now use the debugger to see how control flows between files.

  1. Set a breakpoint on the first line of main in Main.
  2. Click Debug, then the green Start button.
  3. When paused, click Step Over a few times to:
    • Create the players.
    • Reach the first call to printPlayerStats.
  4. When you reach printPlayerStats(alice);, click Step Into.

You should now be in printPlayerStats:

  1. Step until you reach double avg = ScoreUtils.averageScore(scores);.
  2. Use Step Into to jump into ScoreUtils.averageScore.
  3. In the Variables panel, watch scoreLength, total, and the return value (OnlineGDB does not show array contents reliably).

Answer:

  • Why is the average too small?
  • Which line in which file is the true bug?

4. Use the call stack to reason about a crash

Trigger the NullPointerException on mystery:

  1. Set a breakpoint on the line int total = ScoreUtils.totalScore(mystery); in main.
  2. Click Debug, then the green Start button.
  3. Click Continue exactly 3 times so execution moves past the three printPlayerStats calls and stops on the mystery line.
  4. When stopped there, click Step Into to enter ScoreUtils.totalScore.
  5. Identify the exact line where the exception occurs, then look at:
    • The current line.
    • The Call Stack panel.

Questions:

  • Which expression is actually null?
  • How did that null value get there? (Use the call stack to go “up” one frame.)

Fix totalScore so that:

  • It does not crash when player is null or player.getScores() is null.
  • It returns a reasonable value (for example, 0) in those cases.

Run again to confirm the crash is gone.


5. Fix the best score logic

Currently, bestScore assumes:

  • best starts at 0.
  • Any score greater than 0 is the best.

This fails when:

  • All scores are negative.
  • The array is empty.

Use breakpoints and stepping to:

  1. Pause inside bestScore.
  2. Watch scoreLength and how best changes as you loop through scores.
  3. Decide what should happen when scores is empty.

Update bestScore to:

  • Handle empty arrays gracefully (for example, return 0 or a sentinel like Integer.MIN_VALUE).
  • Correctly track the maximum value even when all scores are negative.

Re‑run the program and check:

  • Do the printed “Best” values now match your expectations?

6. Reflection (Level 3)

  • How did stepping between files help you understand the bugs?
  • When reading a stack trace, how do you choose which frame to investigate first?
  • How can you prevent null‑related bugs like the one in totalScore?

In Level 4, you will tackle a more layered scenario that combines several types of bugs.

Level 4 – Layered Bugs (≈ 10 minutes, stretch)

Concepts: layered bugs, unexpected control flow, combining debugger tools, fixing bugs without breaking other behavior.

This Level uses a small “level loader” that:

  • Reads a level number from a (fake) string.
  • Loads some enemy counts.
  • Computes total difficulty.

The code has:

  • A runtime bug (exception).
  • A logic bug.
  • A bug where stepping into another file is useful.

1. Setup the files

We’ll add two more files:

  • LevelLoader.java
  • InputParser.java

InputParser.java

public class InputParser {

    public static int parseLevel(String input) {
        String[] parts = input.split(":");
        String numberPart = parts[1];

        return Integer.parseInt(numberPart);
    }
}

LevelLoader.java

public class LevelLoader {

    public static int[] loadEnemiesForLevel(int level) {
        if (level == 1) {
            return new int[] {2, 3};
        } else if (level == 2) {
            return new int[] {4, 4, 5};
        } else if (level == 3) {
            return new int[] {10};
        } else {
            return null;
        }
    }

    public static int computeDifficulty(int[] enemies) {
        int enemyCount = (enemies == null) ? 0 : enemies.length;
        int difficulty = 0;

        for (int i = 0; i <= enemies.length; i++) {
            difficulty += enemies[i] * (i + 1);
        }

        return difficulty;
    }
}

Update Main.java

Add this method to the bottom of Main (inside the class, after printPlayerStats):

    private static void demoLevels() {
        System.out.println("=== Level Loader Demo ===");

        String input = "level: 4";
        System.out.println("Parsing input: \"" + input + "\"");

        int level = InputParser.parseLevel(input);
        System.out.println("Parsed level: " + level);

        int[] enemies = LevelLoader.loadEnemiesForLevel(level);
        int enemyCount = (enemies == null) ? 0 : enemies.length;
        int difficulty = LevelLoader.computeDifficulty(enemies);

        System.out.println("Total difficulty for level " + level + ": " + difficulty);
    }

enemyCount is there so you can inspect length in the debugger (OnlineGDB does not show array length reliably).

Then, in main, after the mystery player block, add:

        demoLevels();

Make sure Main.java now:

  • Uses Player, ScoreUtils, LevelLoader, and InputParser.

2. Run and observe

  1. Open Main.java.
  2. Click Run.

Expected symptoms:

  • The scoreboard section should behave as in Level 3 (assuming you fixed it).
  • The Level Loader demo will likely:
    • Throw a NumberFormatException or NullPointerException.
    • Or print a strange difficulty.

Look at:

  • The exception type.
  • The stack trace: which method and line caused it?

3. Step through the layered flow

Use the debugger to trace the whole path:

  1. Set a breakpoint at the first line of demoLevels.
  2. Click Debug.
  3. Click the green Start button.
  4. When paused:
    • Step Over the print statements.
    • Use Step Into on InputParser.parseLevel(input).

Inside parseLevel:

  1. Inspect input, parts, and numberPart.
  2. Step line-by-line and identify exactly where and why the first failure happens.

Once you understand the problem, stop debugging and fix parseLevel to:

  • Validate the input format.
  • Trim whitespace.
  • Return a reasonable default (for example 1) or throw a clearer exception.

Run again with the debugger to confirm the parsing step works.


4. Follow the next bug after parse is fixed

Next, set a breakpoint on:

int[] enemies = LevelLoader.loadEnemiesForLevel(level);
  1. Debug again and Step Into LevelLoader.loadEnemiesForLevel.
  2. For the given level, see which branch runs and what it returns.
  3. Fix the null issue you observe first.
  4. Re-run Debug and Start.
  5. Step back to demoLevels and then Step Into computeDifficulty.

Inside computeDifficulty (only after the null-related issue is fixed):

  1. Watch the values of i, enemyCount (the local int in computeDifficulty), and difficulty.
  2. See what happens when i reaches the same value as enemyCount.
  3. Use that to isolate the loop issue.

Fix the bugs by:

  • Making loadEnemiesForLevel return a non‑null array, even for unknown levels (for example, an empty int[]).
  • Adjusting the for loop in computeDifficulty so it:
    • Does not go out of bounds.
    • Handles enemies being null or empty safely.

Re‑run and confirm:

  • No exceptions occur.
  • The printed difficulty matches your expectations.

5. Avoid introducing new bugs

After each fix, re‑run the whole program, not just the Level Loader demo:

  • Check that:
    • The scoreboard output (from Level 3) still looks correct.
    • No new exceptions appear.

This is a realistic part of debugging:

  • Fix one bug.
  • Run the full program.
  • Make sure you didn’t break something else.

6. Reflection (Level 4)

Short prompts:

  • What kinds of bugs did you see in this Level (compile‑time, runtime, logic)?
  • How did stepping into helper methods (parseLevel, loadEnemiesForLevel, computeDifficulty) help?
  • What habits can help avoid layered bugs (e.g., validating inputs, returning empty collections instead of null)?

You can compare your fixes to the suggested ones in Solutions & Fixes.

Solutions & Fixes

This section shows one set of possible fixes for the workshop exercises.
Your exact solutions may differ slightly as long as they:

  • Remove the crashes.
  • Produce correct behavior.
  • Keep the code readable.

Level 1 – Reading Errors

CalculatorExercises.divide

One reasonable fix:

public static int divide(int a, int b) {
    if (b == 0) {
        // FIX: prevent division by zero and return a safe default
        System.out.println("Cannot divide " + a + " by zero.");
        return 0; // or throw new IllegalArgumentException("b must not be zero");
    }
    return a / b;
}

CalculatorExercises.sumFirstThree

Handle short arrays safely:

public static int sumFirstThree(int[] numbers) {
    if (numbers == null || numbers.length == 0) {
        // FIX: handle null or empty arrays without throwing an exception
        return 0;
    }

    int limit = Math.min(3, numbers.length);
    int total = 0;
    for (int i = 0; i < limit; i++) {
        // FIX: stop before we run past the end of the array
        total += numbers[i];
    }
    return total;
}

LoopExercises.countUpTo

If the intent is to count how many numbers from 1 to n (inclusive):

public static int countUpTo(int n) {
    int count = 0;
    for (int i = 1; i <= n; i++) {
        // FIX: loop up to and including n so the count matches the intent
        count++;
    }
    return count;
}

Level 2 – Debugger Basics

The key fixes here are the same as Level 1, but discovered using:

  • Breakpoints.
  • Stepping.
  • Variable inspection.

See the Level 1 solutions above.


Level 3 – Multi‑File Debugging

ScoreUtils.averageScore

Correct the average calculation:

public static double averageScore(int[] scores) {
    if (scores == null || scores.length == 0) {
        // FIX: avoid dividing by zero and define a default average
        return 0.0;
    }

    int total = 0;
    for (int score : scores) {
        total += score;
    }
    // FIX: divide by the correct length to get an accurate average
    return (double) total / scores.length;
}

ScoreUtils.bestScore

Handle empty arrays and negative scores:

public static int bestScore(int[] scores) {
    if (scores == null || scores.length == 0) {
        // FIX: define behavior for empty or null score lists
        return 0; // or throw an exception, depending on requirements
    }

    int best = scores[0];
    for (int i = 1; i < scores.length; i++) {
        // FIX: compare all elements starting from index 1
        if (scores[i] > best) {
            best = scores[i];
        }
    }
    return best;
}

ScoreUtils.totalScore

Defensive null handling:

public static int totalScore(Player player) {
    if (player == null || player.getScores() == null) {
        // FIX: return a safe default when player or scores are missing
        return 0;
    }

    int sum = 0;
    for (int s : player.getScores()) {
        sum += s;
    }
    return sum;
}

Level 4 – Layered Bugs

InputParser.parseLevel

A safer implementation:

public static int parseLevel(String input) {
    if (input == null) {
        // FIX: handle null input by falling back to level 1
        return 1; // default level
    }

    String trimmed = input.trim(); // e.g., "level: 4"
    String[] parts = trimmed.split(":");
    if (parts.length < 2) {
        // FIX: validate the basic "label:number" format
        return 1; // or throw new IllegalArgumentException("Invalid level format");
    }

    String numberPart = parts[1].trim();

    try {
        // FIX: trim and safely parse the numeric part
        return Integer.parseInt(numberPart);
    } catch (NumberFormatException e) {
        // FIX: use a safe default when parsing fails instead of crashing
        return 1;
    }
}

LevelLoader.loadEnemiesForLevel

Return a non‑null array:

public static int[] loadEnemiesForLevel(int level) {
    if (level == 1) {
        return new int[] {2, 3};
    } else if (level == 2) {
        return new int[] {4, 4, 5};
    } else if (level == 3) {
        return new int[] {10};
    } else {
        // FIX: return an empty array instead of null for unknown levels
        return new int[0];
    }
}

LevelLoader.computeDifficulty

Fix the off‑by‑one and null handling:

public static int computeDifficulty(int[] enemies) {
    if (enemies == null || enemies.length == 0) {
        // FIX: define a difficulty of 0 when there are no enemies
        return 0;
    }

    int difficulty = 0;
    for (int i = 0; i < enemies.length; i++) {
        // FIX: stop at enemies.length - 1 to avoid going out of bounds
        difficulty += enemies[i] * (i + 1);
    }
    return difficulty;
}

Putting it all together

After applying these fixes:

  • The programs should compile and run without exceptions.
  • The scoreboard should report sensible averages, best scores, and totals.
  • The level loader should parse inputs, avoid null issues, and compute difficulty correctly.

If your results differ, use the debugger again to:

  • Set breakpoints in the fixed methods.
  • Step through and verify that variable values match your expectations.