CPSC 211
Introduction to Debugging

Objectives

This lab will introduce you to the Eclipse debugger - an important tool for finding and fixing run-time errors - as well as some of the features that Eclipse provides to help you avoid compile-time errors. In this lab, you will:

  • learn to write better code more quickly using Eclipse's advanced editing features
  • learn the principles of debugging
  • learn how to use Eclipse's graphical debugger

Pre-lab Overview

Introduction

This lab explains general debugging principles and teaches you how to use the Debug perspective in Eclipse. Before coming to the lab, you should read and understand the following:

Types of Errors

There are two major kinds of errors you will encounter when programming in Java: compile-time errors and run-time errors.

Compile-time errors

Compile-time errors include syntax errors (typos, misspellings, missing or illegal keywords, incorrect arguments...) as well as uninitialized variables and missing import statements, and will be detected when you compile your program. As you have probably noticed, Eclipse detects compile-time errors as they occur (as you type) and underlines them in red. It also suggests possible fixes (more on this later).

Note: Sometimes Eclipse gets "stuck" and continues to report an error after you have already corrected it. Saving the file again will cause it to be recompiled and usually fixes this problem. In come cases, it may be necessary to rebuild the entire project (select Project -> Clean... and click OK).

Run-time errors

Run-time errors are detected when the program is run (executed) and are often caused by logical errors in your program design. As a result, they can be extremely difficult to fix - you will probably spend a lot of your time in this course trying to fix run-time errors - however, you may find that you learn a great deal from this process. Run-time errors may result in exceptions being thrown, such as when trying to access an array with an index that is too large, or they may be manifested as incorrect output (for example, a buggy ATM machine might report a negative balance in your account or might dispense the wrong amount of money).

General Principles of Debugging

A debugger allows you to pause (suspend) execution of your program at any line of code (by setting breakpoints) and see:

  • which method invoked the currently executing method,
  • the values of the variables declared in the current method,
  • the values of the variables declared in the calling method,
  • and the values of the member variables of the object(s) to which these methods belong.
Using a debugger, you can run your program one line at a time, called stepping, or run it at full speed, pausing automatically at breakpoints or when a variable of interest is accessed or modified. A debugger can be an invaluable tool in tracking down the source of run-time errors, however, you do not want to spend hours stepping through your code without knowing what you are looking for.

Before using a debugger, it is important to

  1. State the bug as precisely as possible:
    • How is the output incorrect?
    • What would constitute correct output?
  2. Be able to consistently reproduce the bug.

Debugging with Eclipse

When a program is suspended by the debugger, Eclipse highlights the next line to be executed in green and displays a solid blue arrow in the left margin at that line. To resume execution, you can:
  • Execute one line at a time by
    • Pressing Step Over to move to the next line in the current method, or
    • Pressing Step Into to move execution to the beginning of the first method called by that line (if there are any)
  • Run the program at full speed until the next breakpoint (or to the end of the program) by pressing Resume
  • Complete execution of the current method and return to its caller by pressing Step Return

Enhanced for loop

Some of the code in this lab will make use of the enhanced for loop, sometimes called the for-each loop. If you didn't encounter this loop in your previous course, please familiarize yourself with it before coming to the lab. There's a nice introduction in section 7.4 (8.4 in 2nd ed.) of your text.


During the Lab

Getting Started

  • Create the directory ~/cs211/labs/debug
  • Create a new project in Eclipse called DebugLab. Choose Create project from existing source and fill in the directory field with ~/cs211/labs/debug
  • Download this zip file containing all of the files you will need for this lab and import the contents of the zip file into the project. If you don't remember all the steps necessary to configure an Eclipse project to work from an existing source and import the source from a zip file, see the instructions provided in the first lab.

Part 1: Fixing Compile-time Errors in a Bag

Bag.java implements an unordered container that holds up to 100 ints. It supports insertion and removal and also provides a method to count the number of occurrences of an element in the bag. Unfortunately, Bag contains some compile-time errors, as well as some run-time errors. BagDriver is a simple (and bug-free) class to test the bag.

Fixing the Compile-time Errors

  1. Open Bag.java in Eclipse and notice the red boxes along the right margin, each of which identifies a compile-time error. Each error is also listed in the Problems view at the bottom of the window and files with errors are marked with an X in a red box in the Package Explorer on the left. Move the mouse over the first (top-most) box and a description of the problem pops up. Click on the box and the error is highlighted in the editor.
  2. In the left margin, each line that has an error is marked with a light bulb or an X in a red or grey circle (these are meant to indicate different things but Eclipse does not use them consistently). Click on this to see if Eclipse has suggestions for possible solutions, called quick fixes. A box listing quick fixes appears, and selecting an item pops up a code snippet showing the results of applying the suggested change. Double click Initialize variable to apply the change, and notice that two errors have disappeared. Notice also that the default of initializing answer to zero is correct in this case as we are counting the number of occurrences of an element in a Bag, but that in other cases the default action taken by Eclipse may not be exactly what you want.
  3. Move to the next error and click in the left margin. A popup informs you that a semi-colon is missing. Add the semi-colon and move on to the last error, a missing ). Insert the missing ). Did a new error appear? Fixing errors often causes Eclipse to detect previously unnoticed errors as it re-parses the file.
  4. Eclipse tells you that there is no printl method in the PrintStream class and suggests two possible replacements: print and println. Change printl to print.
Bag should now be free of compile-time errors; later on we'll use the debugger to fix the run-time errors

Part 2a: Learning to Use the Eclipse Debugger

Names.java is a silly class who's only purpose is to help introduce you to the debugger. It has an array of Strings and two methods: printNames which prints the contents of the array and setNames which sets all the Strings to have the same value. The main method is supposed to create a Names object, print out its (default) contents, change all the Strings to "Sally" and print out the contents again. You might be able to spot the bug just by looking at the code but you should still practice using the debugger. Note that this code uses a 'for each' loop. This is very similar to a standard for loop. If you're not familiar with it, there's a good description in your text book.

  1. Open Names.java in Eclipse and run it as a Java application, observing that it does not work correctly. Be sure that you can clearly state what the problem is.
  2. It seems likely that the bug is in the setNames method, so it would be nice if we could see what is happening there. We can do just that by setting a breakpoint at the setNames method. Eclipse provides several equivalent ways to do this:
    • Put the cursor somewhere in the setNames method and choose Run -> Toggle Method Breakpoint,
    • Right click in the left margin next to the very first line of the method (the method declaration) and choose Toggle Breakpoint, or
    • Just double click in the left margin next to the line where you want to put a breakpoint.

    A blue circle should appear in the left margin. Right click on this circle and choose Breakpoint Properties.... A dialog appears allowing you to specify various properties such as
    • whether the debugger should suspend the program on entering the method (the default), on exiting, or both,
    • whether the debugger should suspend the program every time the breakpoint is reached or only after it has been reached a certain number of times,
    • whether the debugger should suspend the program every time the breakpoint is reached or only when a variable or variables have certain value(s) (called a conditional breakpoint),
    • and whether to suspend only the thread that reached the breakpoint or the entire Java Virtual Machine (you don't need to worry about this now).
    We don't need these options for now, so click Cancel to close the dialog.
  3. To start the debugger, click Run -> Debug As -> Java Application. If Eclipse asks if you want to switch to the Debug perspective, click OK.
  4. You should now be looking at the Debug perspective. On the top left, the Debug view shows each thread of the program including whether it is suspended and why. It also shows the call stack for each thread, with the currently executing method (Names.setNames in this case) on top and the first method called by the thread (Names.main) on the bottom. The Variables view on the top right shows the variables that have been declared in the current method and their current values, including the this variable which references the current object. Each object in the Variables view has a plus sign next to it which you can click to expand it and see the values of its member variables. Selecting Names.main in the Debug view changes the Variables view to show the variables that were declared in the main method before the call to setNames. It also adjusts the editor to show the main method and highlights the current line in that method.

    Select setNames in the Debug view and, in the Variables view, expand this (which refers to the Names object that was created in the main function) to reveal the names member variable, an array of 5 Strings. Select names to display its value in the bottom part of the Variables view. Step one line at a time through the method, observing the values of names and str as the for loop advances. Why are the changes in str not reflected in names?

Watch Your Step!

When stepping through a program, you should normally use Step Over. When you come to a call to a method that you think might have a bug (or if you just want to see what it's doing), you can use Step Into to enter that method. Note that you usually can't step into calls to built in Java classes (such as System.out.println() or methods of the String class), and you don't want to because you aren't likely to find any bugs in them. However, if you suspend the debugger while one of these methods is executing you will step into it (regardless of whether you used Step Into or Step Over) and this can be confusing as you will often see a lot of methods piled on top of the call stack in the Debug view. If this happens, or if you accidentally step into a method you wanted to step over, press Step Return to leave the current method (and pop it off the call stack) and repeat as necessary until you are back in your own code.

Set a breakpoint on main and run Names with the debugger again. Try stepping through it using the Step Into, Step Over and Step Return buttons to be sure you understand the difference.

To stop the debugger and abort the currently executing program, select the application in the Debug view and press Terminate on the debug toolbar or in the Run menu, or right click the application and select Terminate.

Part 2b: Reindeer Games

Reindeer.java is supposed to display each reindeer once, but it has a bug. Set a breakpoint at an appropriate place and run it with the debugger to see what is going wrong. What is the bug?

Part 3: Fixing the Run-time Errors in the Bag Class

We now return to the Bag class from part 1. Now that you've fixed the compile-time errors and you know how to use the debugger, you should be able to find and fix the run-time errors.

  1. Run BagDriver and look at the output for evidence of run-time errors.
  2. Use the debugger to track down and correct all the bug(s) you find, following the procedures described above.
  3. Repeat until you are satisfied that your Bag works correctly. Hint: If your program sits for a long time doing nothing when you run the debugger, you can click Suspend to see where it is stuck and look at the values of variables, but remember that running a program with the debugger is always slower than running it without.
  4. Show your corrected Bag to your TA.

Part 4: More on Compile-time Errors

The Books class holds an array of book titles (Strings) in alphabetical order and allows you to print its contents. The find method is supposed to take a book title and, if it is in the array, return its index in the array, starting from zero, and otherwise return the index at which it should be inserted to preserve alphabetical ordering.

  1. Run Books and observe that the output is incorrect. The bug is in the goesBefore method which contains only one line, return book1.equals(book2);. Hold the mouse over the call to the equals method of the String class and the documentation for the method pops up. It should be clear that the equals method is of no help to us here.
  2. Delete everything after return book1. until the end of the line. Make sure the cursor is immediately after the . and press Ctrl + Space to bring up a list of completions, including all the public methods of the String class. Clicking an item in the list pops up its documentation. Look for a method that will allow goesBefore to work correctly. You only need one line of code, and you should only have to call one method. Look at the method names and documentation to determine which method to use, and how to use it. Hint: The method you want begins with the letter c - type c after the dot to restrict the list to items beginning with a c (if necessary, press Ctrl + Space again to bring back the list).
  3. Double click the correct method to insert it into your code, and complete the goesBefore method. Run the program to ensure that the output is now correct and show it to your TA.
Eclipse's ability to detect errors as they happen, suggest and automatically apply quick fixes, and provide integrated documentation is probably its most powerful feature and you should take full advantage of it as it will save you a lot of time. If you want to see documentation for your own code integrated into Eclipse (this can be very useful for larger projects), you need to comment your methods in the style used in Bag.java.

Note: If you are working at home, you may need to attach the source files for the Java libraries in order to see documentation. Hold Ctrl and click on the equals method, then click on the Attach Source button that appears and browse to the directory where your Java Development Kit (JDK) is installed (in Windows, usually C:\Program Files\Java\jdk1.5.0_01 or similar) and select src.zip.

Code Generation and Refactoring

Another extremely useful feature of Eclipse is that it can automatically generate methods and alter code across an entire project. To access these capabilities, you can select a package, class, method or field in the Package Explorer or the editor and look at the options under the Source and Refactor menus. Most of these options are more advanced than you will need in this course, but two will be very useful:
  • you can use Refactor -> Rename... to rename variables, fields, methods, parameters, classes and packages such that references to them (eg. method calls) in the same file and in other files are automatically updated.
  • you can use Source -> Format to automatically format your code so that the lines are not too long and everything is indented correctly and spaced for readability. You should always format your code consistently to make it easier to read.

OPTIONAL Part 5: More Debugging - Minimax Tic Tac Toe

MinimaxTicTacToe is a class that allows you to play a game of tic tac toe against the computer. It uses TicTacToeGame to represent the tic tac toe board, and implements the minimax algorithm to look at possible future moves to ensure the computer can't be beaten. Don't worry, you do not need to understand the details of the algorithm nor the workings of the TicTacToeGame class to do this exercise. In fact, you can assume that the TicTacToeGame class and the methods maxMove and minMove of the MinimaxTicTacToe class are free of bugs.

  1. Run MinimaxTicTacToe. The program is supposed to repeatedly display the game board, prompt you for a move, and display an updated game board with your move and the computer player's response. Convince yourself that there is a bug in the program (remember, it should be impossible to beat the computer, but the computer shouldn't be allowed to cheat!).
  2. Use the debugger to find the bug. You should step over the calls to methods you know do not contain bugs and step into methods that you are suspicious of. If you aren't sure where to begin, try setting a breakpoint somewhere in the play method and watching the values of the variables as the game progresses, particularly when you know something strange is about to happen. Remember that you can expand objects in the variables view to see their contents, and note that the TicTacToeGame class represents the game board as a 3x3 array of ints where 1 represents x and -1 represents y.

    Conditional Breakpoints

    It may be useful to use conditional breakpoints. As an example, to stop execution after you have entered 5 (the center square) as your move, you would: set a breakpoint on the line immediately following the call to promptForMove(), right click it and choose Breakpoint Properties..., check Enable Condition and Suspend when: condition is true and type the following in the box:

    return move.x == 1 && move.y == 1;

    You can also have more complicated conditions, however Eclipse will not detect errors in your conditions until you run the debugger so it's worth double checking your code. You can think of the condition as a function that returns a boolean indicating whether to suspend the program but which has access to all the same variables and methods as the line of the code at which you set the breakpoint. To stop execution when the program reaches the beginning of the while loop in the play() function and the computer has played in the first 2 squares of the first row, you would set a breakpoint on that line, right click it and choose Breakpoint Properties..., check Enable Condition and type the following:

    int[][] x = game.toArray();
    return x[0][0] == 1 && x[0][1] == 1;

  3. Correct the bug, play a few games to make sure that the program now operates correctly, and show it to your TA.