Lab 1

Contents:

Goal

The purpose of this lab is to equip you with the tools that you need to debug run-time errors in your own software. You may find these useful when dealing with complex systems whose behavior is difficult to observe using other techniques. We will focus on the Eclipse debugger. After you finish this lab, you should be able to use Eclipse to follow the sequential execution of your code by examining state values at every point. This will allow you to better understand programs you write, and fix errors more efficiently.

The focus of this lab is not on coding.

Steps to Debugging

To debug effectively, you need to:

  1. Isolate a particular test case (input) for which the expected output does not match the actual output
  2. Identify the method where the error occurs, or where you can first observe the symptoms
  3. Understand the error that causes the symptoms

Basic Debugging Tools

JUnit provides a good environment for finding where test cases fail (point 'a' above), provided that you have good test cases. Here is a list of ways that can help with steps b and c:

For the purposes of this lab, we will focus on the first three methods shown above.

toString()

Every class has a toString method. Classes that do not implement this method explicitly inherit a plain, uninformative implementation of it from Object. Writing a custom toString will enable you to inspect the state of an object if you need it for debugging. Printing the object's class in this string is often useful.

During debugging, it may be helpful to use a toString method that prints every field in your class, potentially with a field-name label, as well as the name of the object's class.

For example, GeoSegment.toString() from Problem Set 2 might look like:

public String toString() {

    return "GeoSegment: " + name + "\n  p1: " + p1.toString() + "\n  p2: " + p2.toString();

}

and GeoPoint.toString() might look like:

public String toString() {
 
    return "GeoPoint: (" + latitude + ", " + longitude + ")";

}

Then, calling toString() for GeoSegment would output something like:

GeoSegment: University Way NE
   p1: GeoPoint: (47661193, -122313087)
   p2: GeoPoint: (47659762, -122313130)

Verifying method parameters with System.out.println

It is often useful to verify that paramters passed to methods do infact satisfy that method's "@requires" clause. Even if you are sure that the variables you are passing are correct, it can still save you valuable debugging time to check! Using several System.out.println's at the top of a method is a good place to start. For example, from Problem Set 1, the beginning of the scaleCoeff method might output something like:

Entering method scaleCoeff(Vector<RatTerm> vec, RatNum scalar)
this: x + 2
vec: [Term<coeff: 1 expt: 1>, Term<coeff: 2 expt: 0>]
scalar: -2

In this case we can clearly see that the vector contains two RatTerms and we are multiplying the RatPoly by the scalar -2.

When verifying method parameters, you should always include the entire method signature in the output. This helps isolate which particular method is being called, since it can be ambiguous with Java's method overloading, where multiple methods can share the same name but take different parameters.

Though print statements are a quick way to view the state of a program at a single point, they have several disadvantages. Printing information to the screen may generally be confusing since multiple lines of output may appear instantaneously on the screen, making it difficult to interpret them. Also, print statements have to be removed from the final version of the program, which can be a difficult and tedious task.

checkRep() methods

Writing a checkRep() method can be hugely beneficial because it allows you to track down the first time that your object's internal representation invariant (RI) is broken. The method should verify that the RI is satisfied through assertions. You can use JUnit's assertion framework or devise your own scheme.

Inserting calls to checkRep() at the beginning and end of public methods will allow you to be notified instantly when your representation is broken. This is especially important when using mutable classes. Immutable classes, on the other hand, only need to have their representation checked at the end of the constructor. If the representation is correct there, it will be correct everywhere.

Eclipse Debugger

A debugger is a program that lets you interact directly with running software. This enables you to see the state of the program as it runs. The debugger allows you to add breakpoints to specific lines of code - when the program reaches a breakpoint, execution is paused so that the values of variables can be viewed. Program execution can then be resumed from that point, and the program will continue running until it is finished or another breakpoint is encountered. The debugger also allows line-by-line execution of code so that you can see exactly how particular lines of code affect variables of interest.

Debugging programs

In Eclipse, you can execute a program with or without debugging tools enabled. To execute without debugging tools, use the "Run" menu option; to execute with debugging tools, use the "Debug" menu option.

When a program is being debugged, we enter something called "debug mode." We can enter that mode when we're not debugging a program, too, and sometimes we may want to. In order to enter Eclipse's debug mode, from the top menu, select:

Window >> Open Perspective >>Debug

Or, if that does not appear, select:

Windows >> Open Perspective >> Other...

and select the Debug option from dialog that appears as in the screenshot below:

Debug Dialog Box

If you have not checked out the code for this lab, do so now (following the same procedure you would for a problem set, as outlined here). Then, open all three source files for the lab (BugRidden.java, IntegerComparator.java, and Lab1.java) and switch to debug mode.

Layout

You should now see the default Debug perspective, which consists of five main sub-windows (called views in Eclipse), organized into 3 rows.

Debug Perspective

To run the program that is currently displayed in the editor view, select the down arrow next to green and white run arrow (Green Arrow) on the menu at the top of the screen and select:

Run As >> 1 Java Application

Subsequent runs of your program can be performed by simply clicking the green and white arrow or selecting the name of the Java file from the list provided in the pop-up menu.

To utilize Eclipse's debugging tools for the program that is currently displayed in the editor area, select the down arrow next to the bug icon (Down Arrow) at the top of the screen and select:

Debug As >> 1 Java Application

Subsequent debugging of your program can be performed by simply clicking the bug icon or by selecting the name of the Java file from the list provided in the pop-up menu.

Note that you may stop a currently executing program (for example, one running in an infinite loop) by selecting it in the "Debug" view (at the top left) and pressing the red square in the top bar of that view, or by selecting Run >> Terminate from the menu.

Breakpoints

Eclipse helps you to debug more efficiently by allowing you to mark special lines of interest in the program. These 'special lines' are called breakpoints. When running the program with the debugger, the execution of a program will suspend just before the execution of the line with the breakpoint on it. Multiple lines may have breakpoints - there is no limit on how many you can use - so code execution may suspend multiple times. The state of the program upon suspension is the same as the state of the program just prior to suspension - that is, the code has run completely normally until this point and now it is just taking a break. This means that the state of the program (variable values) during execution is saved.

Once the program reaches a breakpoint, the Eclipse debugger allows you to perform a line by line execution of your code and enables you to monitor the status of variables to ensure that your program is running correctly. These functionalities are described in the next two sections.

There are two main ways to add a breakpoint in Eclipse. The first is to double click on the marker bar to the left of the editor window (shown by a grey margin) next to the line to which you want to add a breakpoint. The alternative is to right click somewhere on the line interest and to select "Add Breakpoint" from the pop-up menu instead. A breakpoint is indicated by a blue circle in the grey margin to the left of the line to which it has been applied.

Select the file Lab1.java in the "Editor" view. Add a breakpoint to line 16 (list.add(2*i+1)). Eclipse displays line information in the bottom status bar in the format line:column. Now start the debugger (as explained earlier). Eclipse may ask you if you want to enter the "Debug" view. Click "yes" and you should find yourself back in "Debug" view. The program will execute. You should observe that the program executes up to the breakpoint, and when it reaches line 16, stops and Eclipse highlights that line.

Line by line execution of code

Once a program is suspended with a breakpoint, a debugger provides you with the power to execute subsequent code one line at a time.

Step over and stepping into a line of code

"Stepping over" means allowing the program to execute to the next line of code in the text editor. "Stepping into" can be used to "go inside" the currently executing line of code. This is a bit less intuitive but is easily shown by example: Let's say that in PS2's GeoSegment class you wrote something like:

public double getLength() { 
    
	return p1.distanceTo(p2);
    
} 

Here, you are delegating the work of calculating the distance between the two points in the GeoSegment to the GeoPoint P1's 'distanceTo' method. If you were to break on this line and then choose to step over the line, you would end up on the line with the closing brace. This isn't terribly useful if we think that something may be breaking in the distanceTo method. So, instead we can choose to "step into" the line. This will take us inside GeoPoint's distanceTo method and allow us to step around inside it, giving us a better picture of the inner-workings of the class.

To step over a line of code, click on the step over button in the upper left debug view, or press the F6 key. You can also right click on the suspended thread in the debug view, indicated by 3 horizontal blue bars, and select the step over option in the pop-up menu.

To step into a line of code, click on the step into button in the upper left debug view, or press the F5 key. You can also right click on the suspended thread in the debug view, indicated by 3 horizontal blue bars, and select the step into option in the pop-up menu.

Step over the currently selected line of code, and watch as line 15 is highlighted. This is because the debugger reached the end of the first iteration in a for loop, and is starting again at the beginning. Leave your code paused in debug mode and continue reading for the next steps.

Variable Traces

Variable traces allow you to monitor the values of variables at any given point in your program. By checking the states of variables as your program executes, you can check where you have wrongly assigned values to variables, as well as check when a representation has gone awry. Thus, variable traces reduce the hassle of needing to write debugging statements that print out the states of variables during run-time.

On the upper right view, click on the "Variable" tab. As you step over or step into lines of code, this "Variable" view will display values and states of local variables. If a variable refers to an object, clicking on the "+" sign (or triangle) next to the variable reveals all fields in that object. This allows you to inspect the state of an object easily. The object can be browsed as a tree. You will play around with this feature shortly.

Note the value of the variable i in the "Variable" view (it should be 0). Continue stepping over the code in the for loop and watch as the value of i increases each time through the loop. You can view the state of the list variable and its contents by expanding its variable node (as explained in the previous paragraph). Note how the list.add(...) statement affects the list variable. After a few runs through the loop, you can stop the debugger by clicking the red square in the top bar of the "Debug" view.

In the following screenshot, we see elements at indices 8 through 15 of a list in the Eclipse Variables view. The elements in the list contain Integer objects, which can be expanded to show the fields in these objects (in this case, the int values they contain).

variables

The Eclipse debugger allows you to skip over parts of the program that you know are working correctly and to isolate the problem areas by setting breakpoints and tracing those lines of code. The debugger has the advantage of monitoring the program's behavior (through line by line execution) without the messiness of System.out.println(). It is much easier to add a breakpoint to your program and inspect the state of variables than to write out redundant System.out.println() statements around the lines of interest.

Problems

Problem 1: Monitoring Variable Values

Take a look at the class in BugRidden.java. This contains a function convertToArray(List l) which is supposed to take in a List, remove all of its elements, and return them as an array. Unfortunately, the procedure has a couple of bugs and does not work as expected. More specifically, as you can see by compiling and running Lab1.java, when we call convertToArray(List l) with a List of all the odd numbers from 1 to 199, i.e.,

List l = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199]

According to its specifications, convertToArray should return an output array that contains all the elements above, and it should also empty the original list. But here is the array we get:

Object[] a = [1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 169, 173, 177, 181, 185, 189, 193, 197, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]

The original list should be empty; instead, after the function is called, the List is:

l = [3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 67, 71, 75, 79, 83, 87, 91, 95, 99, 103, 107,111, 115, 119, 123, 127, 131, 135, 139, 143, 147, 151, 155, 159, 163, 167, 171, 175, 179, 183, 187, 191, 195, 199]

As you can see from the output above, convertToArray is only removing every other element of its input list, and only placing those elements into the output array. Using the Eclipse debugger, isolate the problem by stepping through the code provided. You should add a breakpoint to the beginning of convertToArray; once Eclipse has suspended execution of the program, you should examine the members of the input list and the output array, noting the changes made during each iteration of the for loop. Fix the bugs and continue.

Problem 2: Fixing an Infinite Loop

Now look at the binarySearch(List list, Integer key, Comparator comp) procedure in BugRidden.java. As in problem 1, we have placed some test code into Lab1.java. If you run that code, you'll notice that this iterative procedure does not work correctly and loops infinitely - the program never completes. In addition, there are two more bugs in the code provided. Using the Eclipse debugger to set breakpoints and monitor local variables, you should determine what these bugs are. You should employ a technique very similar to the one you used in the previous problem, focusing on the changes made during the iterations of the for loop. Fix the bugs and continue.

Problem 3: Examining the Stack

Finally, take a look at the binarySearch(Integer[] arr, Integer key, Comparator comp, int low, int high) procedure in BugRidden.java. Like in problem 2, this procedure loops infinitely; this time, however, the function is recursive, so the error appears in the form of a StackOverflowError. As in the previous problems, you should use the Eclipse debugger to isolate and identify the problem (start with a breakpoint, etc.); this time, though, try examining the stack in Eclipse, noting the values of local variables at each level of recursion. This can be done by using the top-left (Debug) view in Eclipse's Debug perspective. You can see the current stack and inspect the values of variables as they were when each procedure on the stack was executed. Examining the variable values is exactly the same as in the previous problems. Fix the bugs and run Lab1.java again. You should get the correct output for all queries to binary search.

Congratulations! You've succesfully debugged a Java program using the powerful Eclipse debugging tools. This skill is incredibly valuable as your systems become more complex and bugs are harder to pinpoint.