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

Start Guide (≈ 5 minutes)

This workshop uses OnlineGDB – a free, modern, browser‑based IDE that supports multiple Java files and a full debugger (breakpoints, stepping, variable watch, call stack) 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 main editor and a Run and Debug button at the top.

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


2. Create the project layout

We will work with several small Java files.

  1. In the File panel (left side), click New File.
  2. Name the file exactly: BuggyCalculator.java.
  3. Repeat and create these files as well:
    • BuggyLoop.java
    • GameApp.java
    • Player.java
    • ScoreUtils.java
    • LevelLoader.java
    • InputParser.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.


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 name in the left panel (for example BuggyCalculator.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 whichever file contains the public static void main(String[] args) method.
In this workshop, Main.java (or GameApp.java, when specified) will be the entry point.

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 (for example Main.java or GameApp.java).
  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. When the program hits a breakpoint, it will pause and highlight the current line.

6. Stepping through code

When the program is paused on a breakpoint, use the toolbar buttons above the editor:

  • 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, watches, 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).
  • Watch expressions:
    • Use the Watch panel (if visible) to add an expression (e.g., scores.size()) and track its value as you step.
  • 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.

You will use all of these while working through the Levels in this workshop.


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. 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 (≈ 5 minutes)

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)

Goals

By the end of this workshop, you should be comfortable with:

  • Finding and understanding:
    • Compile‑time errors
    • Runtime exceptions
    • Logic bugs (wrong behavior, no crash)
  • Using the debugger (not just System.out.println):
    • Setting and using breakpoints
    • Stepping over / into / out of methods
    • Inspecting variables and object state
    • Reading the call stack to trace where a bug starts
  • Working with multiple files:
    • Stepping into helper classes
    • Following control flow across files
    • Fixing one bug without introducing another

Structure and pacing

Each Level follows a similar pattern:

  1. Setup – copy/paste the starter code into the right file(s).
  2. Run and observe – see what error or wrong behavior appears.
  3. Investigate – use the debugger (and sometimes print statements) to narrow down the issue.
  4. Fix – change the code to correct the bug.
  5. Reflect – short questions to connect the bug to a concept.

Instructor suggestion:

  • Pause briefly between Levels to:
    • Ask a couple of check‑in questions.
    • Share a “live” debugging demo if many students are stuck.

Solutions for all Levels are collected in the Solutions & Fixes chapter so you can review them after class (or use them as backup during class).

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 programs:

  • BuggyCalculator.java – has compile‑time and runtime issues.
  • BuggyLoop.java – has a logic bug (off‑by‑one).

1. Setup the files

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

BuggyCalculator.java

public class BuggyCalculator {

    // Intentionally buggy method: sometimes crashes, sometimes returns wrong result
    public static int divide(int a, int b) {
        // BUG 1: possible division by zero (runtime error)
        return a / b;
    }

    public static int sumFirstThree(int[] numbers) {
        // BUG 2: potential ArrayIndexOutOfBoundsException
        int total = 0;
        for (int i = 0; i <= 3; i++) { // off‑by‑one AND assumes at least 4 elements
            total += numbers[i];
        }
        return total;
    }
}

BuggyLoop.java

public class BuggyLoop {

    // Intentionally buggy method: tries to count from 1 to n
    public static int countUpTo(int n) {
        int count = 0;
        // BUG 3: incorrect loop condition, never reaches n
        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 = BuggyCalculator.divide(10, 0);
        System.out.println("Result: " + result);

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

        // Example 3: loop
        System.out.println("Trying countUpTo(5)...");
        int count = BuggyLoop.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 BuggyCalculator.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 BuggyCalculator.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 BuggyLoop.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 or write short answers:

  • 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?

You will revisit these same bugs in Level 2, but this time with breakpoints and stepping.

Level 2 – Debugger Basics (≈ 12 minutes)

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

We will reuse the Level 1 code, but now solve the bugs with the debugger instead of just reading messages.


1. Confirm your code

You should still have:

  • BuggyCalculator.java
  • BuggyLoop.java
  • Main.java

If you already fixed the code in Level 1, temporarily undo those fixes (or paste the original versions from Level 1) so that:

  • divide can still divide by zero.
  • sumFirstThree still loops to i <= 3.
  • countUpTo still uses for (int i = 1; i < n; i++).

We want the bugs back so we can practice debugging them.


2. Set your first breakpoint

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

Start the debugger:

  1. Click Debug.
  2. When execution pauses, the breakpoint line will be highlighted.

3. Step over and inspect variables

When paused at the breakpoint:

  1. Look at the Variables/Locals panel:
    • You should see args and any local variables.
  2. Click Step Over:
    • This executes the divide call and moves to the next line (or throws an exception).

If an exception is thrown:

  • Notice how the debugger shows the line that crashed.
  • Compare to the stack trace from Level 1.

Repeat, but this time:

  1. Set a breakpoint inside BuggyCalculator.divide, on the line return a / b;.
  2. Start Debug again.
  3. The debugger will stop inside divide.

Use the debugger to answer:

  • What are the values of a and b right before the division?
  • What happens if b is zero?

4. Step into another method

Practice Step Into and Step Out with sumFirstThree:

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

You should now be inside BuggyCalculator.sumFirstThree.

While stepping through the loop:

  1. Watch i, total, and numbers.length in the Variables panel.
  2. Use Step Over to run each iteration.
  3. Notice when i becomes an invalid index.

Optional: add a Watch expression (if available) for numbers[i].

When you understand why it crashes:

  1. Click Stop to end the debugging session.
  2. Fix sumFirstThree so it:
    • Does not crash when the array is short.
    • Still correctly sums up to the first 3 items when they exist.

5. Debugging a logic bug with stepping

Now fix BuggyLoop.countUpTo using the debugger, not just by staring at the code.

  1. Set a breakpoint in Main on:
    • int count = BuggyLoop.countUpTo(5);
  2. Click Debug.
  3. When paused, click Step Into to enter countUpTo.
  4. Use Step Over to step through each loop iteration.

Observe:

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

Use this observation to:

  1. Decide what the loop should do.
  2. Update the loop condition so countUpTo(5) returns the expected value.

Re‑run with the debugger to confirm the fix.


6. 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 to debug across multiple files and classes in Level 3.

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:

  • GameApp.java – main program that simulates a few players.
  • Player.java – represents a player and their scores.
  • ScoreUtils.java – helper methods with bugs (logic + bad arguments).

1. Setup the files

Create or open 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 {

    // BUG 1: incorrect average calculation (logic bug)
    public static double averageScore(int[] scores) {
        int total = 0;
        for (int i = 0; i < scores.length; i++) {
            total += scores[i];
        }
        // BUG: divides by (length + 1), making average too small
        return (double) total / (scores.length + 1);
    }

    // BUG 2: unexpected control flow when scores is empty
    public static int bestScore(int[] scores) {
        // BUG: returns 0 even if there are negative scores or empty array
        int best = 0;
        for (int score : scores) {
            if (score > best) {
                best = score;
            }
        }
        return best;
    }

    // BUG 3: bad method arguments / null handling
    public static int totalScore(Player player) {
        // BUG: does not guard against null player or null scores
        int sum = 0;
        for (int s : player.getScores()) {
            sum += s;
        }
        return sum;
    }
}

GameApp.java

public class GameApp {
    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[] {}); // empty scores

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

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

        // Intentionally pass a null player to show a null‑related bug
        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();

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

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

Make sure:

  • All three files are saved.
  • GameApp.java contains a main method and will be your entry point.

2. Run and observe

  1. Open GameApp.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 GameApp.
  2. Click Debug.
  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. Watch scores.length, total, and the returned value in the Variables panel.

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. Start Debug and let execution run to that breakpoint.
  3. Step Into to enter ScoreUtils.totalScore.
  4. When the exception occurs, 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 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)

Discuss or write:

  • 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 {

    // Parses level from an input string like "level:3"
    public static int parseLevel(String input) {
        // BUG 1: assumes the string always has "level:" and a valid number
        String[] parts = input.split(":");
        String numberPart = parts[1]; // can throw if input is wrong

        // BUG 2: doesn't trim spaces, can cause NumberFormatException
        return Integer.parseInt(numberPart);
    }
}

LevelLoader.java

public class LevelLoader {

    // Returns an array of enemy counts for the given level.
    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 {
            // BUG 3: unexpected control flow – returns null instead of empty array
            return null;
        }
    }

    // Computes a "difficulty" score from enemy counts.
    public static int computeDifficulty(int[] enemies) {
        int difficulty = 0;

        // BUG 4: off‑by‑one and assumes enemies is never null
        for (int i = 0; i <= enemies.length; i++) {
            difficulty += enemies[i] * (i + 1);
        }

        return difficulty;
    }
}

Update GameApp.java

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

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

        String input = "level: 4"; // note the space before 4
        System.out.println("Parsing input: \"" + input + "\"");

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

        int[] enemies = LevelLoader.loadEnemiesForLevel(level);
        int difficulty = LevelLoader.computeDifficulty(enemies);

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

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

        demoLevels();

Make sure GameApp.java now:

  • Uses Player, ScoreUtils, LevelLoader, and InputParser.

2. Run and observe

  1. Open GameApp.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. 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 watch for:
    • ArrayIndexOutOfBoundsException on parts[1].
    • NumberFormatException on Integer.parseInt.

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 null / off‑by‑one bugs

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. Step back to demoLevels and then Step Into computeDifficulty.

Inside computeDifficulty:

  1. Watch the values of i, enemies.length, and difficulty.
  2. See what happens when i equals enemies.length.
  3. If enemies is null, notice how that changes things.

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

BuggyCalculator.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;
}

BuggyCalculator.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;
}

BuggyLoop.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.