Debug Class

Debugging is one of those topics that tutorials and language developers never seem to give enough attention to. A good debugging environment aids the programmer by providing a more user-friendly environment within which to develop products faster, and aids users by helping to produce a more bug-free product.

Visual J Plus Plus v6 supports debugging in three ways: with its first-rate debugger, with its conditional compilation feature, and with its implementation of the WFC Debug class.

Visual J Plus Plus v6 Debugger

The Visual J Plus Plus v6 debugger provides numerous capabilities. Even if you are familiar with the features of other debuggers, you will find it worthwhile to quickly review the features of the Visual J Plus Plus v6 debugger. A thorough explanation of the proper use of a debugger is beyond the scope of this tutorial, but I'll touch on some of the most commonly used features.

Setting breakpoints

The most important capability of any debugger is the ability to set breakpoints. A breakpoint is a flag indicating that program execution is to stop at a particular line of code. You can set breakpoints only on executable statements; that excludes declarations, import statements, package statements, and comments.

The simplest way to set a breakpoint is to place the cursor on the line that you want to be a breakpoint and then click the Insert Breakpoint button on the Debug toolbar. (Selecting Insert Breakpoint from the Debug menu or pressing the F9 key works just as well.) A solid red circle in the left margin of the Text editor indicates a breakpoint.

You can set more than one breakpoint before executing the program. As soon as execution reaches any breakpoint that you have set, your program is paused and the Microsoft Visual Studio window reappears. A yellow arrow points to the breakpoint where your program is paused.

Stepping through the program

Once your program is paused at a breakpoint, you can continue program execution by clicking the Continue button on the Debug toolbar or by choosing Continue from the Debug menu. Alternatively, you can step through the program. Stepping means the program executes one statement at a time, pausing after each statement.

An issue arises when you are stepping through a program. When the debugger reaches a function call, does it treat the call as a single statement or does it step into the function and pause at each statement in the function? The debugger enables you to choose how to deal with function calls by providing two commands. The Step Over command treats the function call as a single statement, executing every instruction within the function without pausing until control returns to the calling method. (Step Over is something of a misnomer since the debugger doesn't step over anything.) The Step Into command causes the program to step into the function, pass control to the first statement of the function, and pause execution.

For statements that call functions whose source code isn't accessible, Step Over and Step Into are the same. For example, with library functions Step Into doesn't step into the function because it can't. In this case, Step Into acts just like Step Over.

To terminate execution of a program that is paused, either click the End button on the Debug toolbar or choose End from the Debug menu.

NOTE
A Microsoft Windows-based program that has been paused by the debugger looks like it has crashed; its window won't refresh even when it gains focus.

Displaying debug information

Being able to pause the program isn't much use if you can't see what your program is doing. Fortunately, the Visual J Plus Plus debugger provides several windows to give insight into the program's inner workings. You can open any of these windows by selecting Debug Windows from the View menu and then selecting the appropriate command from the submenu.

To view the values of simple variables while a program is paused, you place the mouse pointer on the variable you are curious about. If the variable is in scope, after a second or two its value appears. However, this method works only for simple variables—variables of an intrinsic type. Besides, going around pointing at each variable can get tiresome if you're debugging a large function.

If you want to see all of the variables defined in your function, open the Locals window. This window displays all variables that are local to the current function along with their current value and type. As you step through your program, values that change are highlighted in red. The Watch window is similar to the Locals window, except the Watch window enables you to select what it displays and it isn't limited to variables local to the current function. Both windows allow you to expand objects to see the values of the objects' data members.

Another useful display window is the Call Stack window. This window displays the name of the current function along with the value of the arguments passed to it. Below that, the Call Stack window displays the function that called the current function and the arguments passed to it. The Call Stack window repeats this process until it reaches the first function. (In the case of a console app, the first function is always main().) Double-clicking any function moves the display to the point where that function was called. A solid green arrow in the left margin points to the call.

Screenshot-1 shows the Visual J Plus Plus debugger with the Locals, Watch, and Call Stack windows visible at one time. In the background, you can see the Text editor window with the source code of the class being debugged. Currently, the program is paused on the breakpoint line designated by an arrow.

The lower left corner of the figure shows the Watch window with a Watch set on this, which has been partially expanded to display many of the internal data members. In the lower right is the Locals window showing the local variables. The call stack trace is in the upper right in the Call Stack window.

Java Click to view at full size.

Screenshot-1. The Visual J Plus Plus debugger showing the main debug windows open simultaneously on the program WFCEcho3.

Setting conditional breakpoints

Often you will want to set a breakpoint that stops the program only when certain conditions are true. For example, if a problem occurs only after the program has processed most of the records in a large data file, you wouldn't want to hit a breakpoint at every record; you would want to pause only on the first record that creates the problem. A conditional breakpoint can make debugging much simpler in instances such as this.

NOTE
The program always stops when a regular breakpoint is encountered. The program stops when it encounters a conditional breakpoint only when a series of conditions are true.

To set a conditional breakpoint, first you set a breakpoint in the normal fashion. Then you choose Breakpoints from the Debug menu. Select the breakpoint you want to edit, and then choose Properties; the dialog box shown in Figure 3-2 is displayed. This figure shows that the breakpoint on line 16 won't pause the program until the tenth time the program passes through line 16 of CapTextWriter.toUpper() and the value of the variable cap is true.

NOTE
In the Java Breakpoint Properties dialog box, the Condition box can contain any Boolean expression. For conditions too complicated to be easily expressed in a breakpoint condition, you can write your own static Boolean function to invoke from the Condition box.

Screenshot

Screenshot-2. The Java Breakpoint Properties dialog box showing a conditional breakpoint.

You can see the details of a breakpoint from the Text editor by pointing at the breakpoint symbol on the left with the mouse pointer. After a second or two, a small window pops up displaying the details of the breakpoint.

Conditional Breakpoints: How Does Visual J Plus Plus Do That?

A conditional breakpoint isn't exactly what it seems. Program execution pauses every time control reaches a breakpoint whether the breakpoint is conditional or not. If the breakpoint is unconditional, control passes to the Text editor immediately. If the breakpoint is conditional, Visual J Plus Plus first evaluates the conditional expression and the hit count. If the conditional expression or the hit count are not satisfied, Visual J Plus Plus immediately continues running the program.

The net effect is that if a condition isn't met, the conditional breakpoint slows program execution somewhat. Exactly how much it slows down depends on how often the conditional breakpoint is hit. For example, a conditional breakpoint set in a small loop will probably slow the program down a lot. Using conditional breakpoints is still a lot better than stepping through code by hand, but program execution speed is something to keep in mind.

Setting a break on exception

Another type of breakpoint is designed to alleviate a frustrating situation that can arise when you test your program. Let's say you know that under a given set of inputs your program aborts by throwing an exception. (If your program handles the exception, it might not abort, but the condition causing the exception still might be unexpected.) You know which exception is being thrown and the input that causes it, but you don't know what condition is causing it to be thrown.

The break-on-exception breakpoint handles this situation by pausing program execution immediately when an exception is thrown. The settings you choose when you set the breakpoint determine whether the program pauses if the exception is handled.

To set a break-on-exception breakpoint, choose Java Exceptions from the Debug menu. From the list of exception classes, select the type of exception you want to trap. If you select Break Into The Debugger, the exception you've selected, plus all subclasses of that exception, are marked with a white "X" in a red ball, as shown in Figure 3-3. (Normally the ball is gray.)

When the exception you've selected occurs, the debugger immediately takes control. You can see at which line the exception was thrown, the value of all variables (by means of the Locals and Watch windows), and a trace of how control got to the exception in the first place (by means of the Call Stack window).

Java Click to view at full size.

Screenshot-3. The Java Exceptions dialog box showing a break-on-exception breakpoint for the IOException class.

Conditional Compilation

It is often useful to add code during debugging that should not be included in the final release version. One common reason for adding this extra code is to display data that would be inconvenient to view through the debugger. Visual J Plus Plus provides for this need by including the #if, #endif, and #define conditional directives.

The #if directive must be followed by a Boolean expression. If the expression evaluates to false, any code appearing between #if and #endif is excluded from compilation. The #if directive is unlike a normal if statement in that the expression following #if can only be defined as one of the following types:

NOTE
This feature is called conditional compilation because the compilation of code between #if and #endif is conditional upon the value of the expression following the #if. I should stress that conditional compilation is a compile-time test involving constants defined by means of either a compiler switch or the #define expression.
NOTE
No Java compiler prior to Visual J++ 6 shares this useful conditional compilation feature with its C++ cousin.

Defining conditional compilation constants in the Project Properties dialog box is the most useful of the three options. For example, the constant DEBUG is automatically defined for the Debug configuration, as shown in Figure 3-4.

Defining DEBUG as shown in Figure 3-4 is equivalent to using the following statement:

#define DEBUG


This in turn is equivalent to the following statement:

#define DEBUG true


Visual J Plus Plus assumes that the value of an undefined compile-time constant is false.

Java Click to view at full size.

Screenshot-4. The Compile tab of the Project Properties dialog box enables the programmer to define compile-time constants quickly.

Since DEBUG is defined under the Debug configuration but not under the Release configuration, I can easily add the following boldface code to the toUpper() function:

/**
 * Capitalize the buffer provided.
 *
 * @param s - string to capitalize
*/
String toUpper(String s)
{
 #if DEBUG
 Text.out.writeLine("Capitalizing:" + s);
 #endif
 // first convert the string into a string buffer
 StringBuffer sb = new StringBuffer(s);
 // and so on…
}


The call to writeLine() is included in the compiled executable file under the Debug configuration but isn't included under the Release configuration.

Using the Debug Class

The classes within the Java libraries, including the classes in the WFC library, are very good about testing for and detecting erroneous conditions. It is uncommon for a Java program to simply crash without an exception being thrown that gives some indication of the problem.

You should follow this example in classes that you write. For example, the CapTextWriter.toUpper(String) method from the WFCEcho3 program discussed in accepts an input String, which it then converts to uppercase. Unfortunately, toUpper() doesn't test the assumption that it will be passed a valid String object; it trusts the program to do the right thing. What if the program passes a null instead?

NOTE
Unlike in C++, a null is the only invalid value that the program could pass.

The following shows the CapTextWriter class updated to test for and handle invalid input.

/**
 * CapTextWrite - a TextWriter that capitalizes
 * output through write(String) or
 * writeLine(String).
 */
import com.ms.wfc.io.*;
import com.ms.wfc.core.*;
import com.ms.wfc.util.Debug;
public class CapTextWriter extends TextWriter
{
 String specialChars = " -.;\t\n";
 /**
 * Constructor.
 * * @param outFile - File object to use for output
 */
 public CapTextWriter(File outFile)
 {
 super(outFile);
 }
 /**
 * Write a line of text after capitalizing each word.
 * * @param s - the string to output
 */
 public void writeLine(String s)
 {
 // the following code outputs a message to the debug
 // console if a null value is passed
 Debug.assert(s != null, "Null string passed to writeLine");
 assert(s != null, "Null string passed to writeLine");
 super.writeLine(toUpper(s));
 }
 /**
 * Write a line of text after capitalizing each word.
 * * @param s - the string to output
 */
 public void write(String s)
 {
 Debug.assert(s != null, "Null string passed to write");
 assert(s != null, "Null string passed to write");
 super.write(toUpper(s));
 }
 /**
 * If the condition c is false, throw an exception
 * containing the text s.
 *
 * @param c - condition to test
 * @param s - string to output
 */
 void assert(boolean c, String s)
 throws WFCInvalidArgumentException {
 if (!c)
 {
 throw new WFCInvalidArgumentException(s);
 }
 }
 /**
 * Capitalize the buffer provided.
 *
 * @param s - string to capitalize
 */
 String toUpper(String s)
 {
 // same as before…
 }
}


If the Boolean value is false, Debug.assert(boolean, String) outputs the String to the debugger's output window. The locally defined assert(boolean, String) throws an exception containing the message string if the Boolean assertion is false.

NOTE
Notice that assert() throws the exception WFCInvalidArgumentException. This is the exception thrown by TextWriter.writeLine(). If a method that overrides another method throws an exception, it must throw the same exception class or a subclass of that exception class.

The DebugTest app tests the updated CapTextWriter class while simultaneously demonstrating another Debug helper method.

/**
 * DebugTest - demonstrate the features of the Debug class.
 */
import com.ms.wfc.io.*;
import com.ms.wfc.util.Debug;
public class DebugTest
{
 /**
 * The main entry point for the app. */
 public static void main (String[] args)
 {
 try
 {
 try
 {
 // create a CapTextFile on standard output
 CapTextWriter out = new CapTextWriter(File.openStandardOutput());
 out.setAutoFlush(true);
 // try a legal test message
 out.writeLine("this is a test string");
 // output object information to debug output
 out.writeLine(Debug.getObjectText(out));
 // now try a null message
 String s = null;
 out.writeLine(s);
 }
 catch(Exception e)
 {
 Debug.printException(e);
 Text.out.writeLine();
 Text.out.writeLine("Exception message follows:");
 Text.out.writeLine(e.getMessage());
 Text.out.writeLine();
 Text.out.writeLine("Stack traceback follows:");
 e.printStackTrace();
 Thread.sleep(2000);
 }
 }
 catch(Exception e)
 {
 }
 }
}


The program begins by creating a CapTextWriter object on standard output. Setting automatic flushing to true ensures that any valid output occurs before the program terminates.

Armed with this output object, the program first outputs a valid string. It follows this by outputting debug information to the debugger's output window. This has effect only when the program is running under the debugger; otherwise, Debug.getObjectText() returns an empty string.

The program then purposely sets a String variable to null and passes this to writeLine().

The exception thrown from writeLine() is caught on the line after the call. First, the catch phrase outputs exception information to the debug window. Again, this Debug function has no effect unless executed under the debugger. Next the exception handler outputs the exception message followed by an exception stack trace.

The output from this program is shown in Figure 3-5.

NOTE
I ran this program by invoking jview /vst DebugTest.class. Doing this generates slightly more detailed stack trace output (the /vst stands for verbose stack trace). This works only if the app was compiled with the debug setting enabled.

Java Click to view at full size.

Screenshot-5. The output from the DebugTest program showing the detailed stack traceback. Comments