In the previous article ( Debugging in Java: Debugging Theory and Strategies ), we covered some basic strategies for debugging in Java. Now we get to the technical details of how to locate and eradicate bugs. The hardest part of debugging is locating the section of code that's gone wrong and determining the reason why. We offer two techniques: trace debugging and logging. Both have their advantages and disadvantages and are suited to different types of tasks.
|"Finding a good trace debugger is essential. The choice of debugger is largely personal. "|
The easiest way to debug software is to use a trace debugger, which allows you to trace through code on a line-by-line basis. Often, bugs are hard to spot because what you think is happening is not, or what you think could never happen, does. People make assumptions about how code works, and sometimes these assumptions can be flawed. Watching the path a JVM takes when going through conditional branches, such as 'if ' or case-switching statements, can provide invaluable information to developers. I can't begin to count the number of times I've made a subtle error in logic during a complex 'if' statement, or assumed the state of a variable and watched a trace debugger jump mysteriously to the wrong section of code.
Finding a good trace debugger is essential. The choice of debugger is largely personal, just as some people prefer a certain type of integrated development environment (IDE). Usually though, you'll feel comfortable with a particular IDE and use the built-in debugger provided. For those using the JDK, there's also a text-based debugging tool. The tool, called jdb, contains much of the functionality of other trace debuggers, but being text-based, requires you to manually type in cryptic commands like "stop at someClass:94". Most developers will prefer to select a commercial debugger and integrated development environment, however.
I've used many types of debuggers, but my preference has to be for Visual J++ and JBuilder (see Figures 1 and 2). Both allow you to see the state of variables, step through line by line, and can handle exceptions and threads. My favorite features of both are the breakpoint and watches. Placing a breakpoint on a certain line will cause code to automatically stop at a certain position, so you can leave code running and then pause when a particular section of code is entered. This saves considerable time, as it removes the need to go through code line by line. Watches, on the other hand, display the state of variables and objects, allowing you to see how they change over time. Such features aren't always required, but are certainly handy to have. In a short program, they aren't as crucial; but if loops are used frequently, you'll find yourself enamored with breakpoints and variable watches.
Pros and cons of trace debugging
Trace debugging allows you to see, in a visual way, how your code is executing, and to watch the state of variables, the stack, and threads. Tools and IDEs that allow trace debugging are the preferred way to locate bugs in Java software. Not only do they make it simpler for the developer and give you a better understanding of how code works, but this method of debugging is also a more efficient way of locating errors. In these days of tight deadlines, and cutting corners, anything that can make debugging time more productive is good for you and for your users.
There is, however, a downside to trace debugging. Applications are best suited to a trace debugger, and to a limited degree, applets. However, most debugging tools that include applet support will function only with a particular browser (for example, Visual J++ works with Internet Explorer and not Netscape). When it comes to other types of Java software, such as servlets or applications running on other virtual machines (such as a Unix box when you develop with Visual J++), you're often on your own, and must resort to other ways of locating bugs. When you can't use a debugging tool, the next best solution is to use logging.
Logging of Program State
The concept of a log is nothing new. Applications have used log files for decades to record transactions and to make note of abnormal situations and irregularities. Unfortunately, when you're trying to locate a bug, just knowing that an error occurred probably isn't going to be very useful. You need to know why it occurred, what events precipitated it, and which section of code was at fault. Logs are very useful, however, during development, as additional debugging information can be stored such as the state of variables or a short message to indicate that a particular section of code has been reached. There are several ways to log the state of an application.
These range from displaying log information to standard output (the user console), displaying information to standard error (a special stream for displaying error conditions, which separates user prompts and information from unusual data), to logging information to a file.
Standard output and standard error
As you are no doubt aware, when a Java application is running, it can send data to the user by writing to System.out, which is a PrintStream object that writes to standard output. A distinction is made between standard output, which should be for normal output, and standard error (represented by the System.err PrintStream object). Status messages sent to either of these streams can be read during debugging, which is useful when your favorite trace debugger can't be used. Simply place println statements strategically throughout your code, to check the state of variables or to show which section of code has been executed. My favorite trick is to echo messages to standard output at critical code branches, like tricky switch sections, or complex conditional statements such as 'while' and 'if '. The next best thing to tracing through code is to flag changes in program state.
Listing 1. Flagging changes in program state.
while ( somecondition ) && ( obj.checkSomething() )
System.out.println ( "somecondition AND obj.checkSomething() = true);
System.out.println ("Loop terminated - somecondition OR obj.checkSomething() = false");
This information is useful for tracing down subtle problems which don't throw an exception or generate an error message. Sometimes, however, the problem will be more overt. You'll have an uncaught exception at runtime, or a caught exception when you're not sure why it was thrown. In these situations, you may want to distinguish between your debugging output, and error conditions, by selecting the System.err stream. Many developers make the mistake of just printing error messages in their exception handlers, but placing additional code in the catch block to output the value of variables can give you a good indication of the cause of the error. From my own experience, this technique often yields unexpected surprises (e.g. out of range variables such as zero for a number, or a negative number). Sometimes this is because an unacceptable variable has been passed as a parameter or data has been formatted abnormally. It can also occur because of a bad implementation of an algorithm or design pseudo-code. Once you know what is happening, though, you can begin to track back and find the cause.
|"Log files aren't as useful as trace debugging, or logging of program state, but they are more compact and can help to show the general location of an error. "|
This information is useful for tracing down subtle problems which don't throw an exception or generate an error message. Sometimes, however, the problem will be more overt. You'll have an uncaught exception at runtime, or a caught exception when you're not sure why it was thrown. In these situations, you may want to distinguish between your debugging output, and error conditions, by selecting the System.err stream. Many developers make the mistake of just printing error messages in their exception handlers, but placing additional code in the catch block to output the value of variables can give you a good indication of the cause of the error.
From my own experience, this technique often yields unexpected surprises (e.g. out of range variables such as zero for a number, or a negative number). Sometimes this is because an unacceptable variable has been passed as a parameter or data has been formatted abnormally. It can also occur because of a bad implementation of an algorithm or design pseudo-code. Once you know what is happening, though, you can begin to track back and find the cause.
For example, consider the following code snippet, which works with a buffer. If format of the data within the buffer is different from what was expected, a parsing algorithm may fail and generate an exception at runtime. Good algorithms handle strange situations gracefully; but in order to understand where something is going wrong, you need to examine the data being processed. Placing code to output the state in the exception handler allows you to see it, without worrying about cluttering the error or output streams unless an actual error is occurring.
Listing 2. Outputting the state in the exception handler.
Writing to standard output and error works great with applications, but what about applets? Well, as strange as it may seem, applets do have a standard output and standard error! If you're using Netscape Navigator or some versions of Internet Explorer, you can view the Java Console, a special window that displays debugging information and applet output. To activate the console in Netscape, go to the 'Communicator' menu, select 'Tools' and then 'Java Console'. If your version of IE supports the console, go to the 'Tools' menu and select 'Options'. Scroll down through the list till you see the Java VM options and choose the 'Java console enabled' option. Not all versions of IE have this functionality, however, and you may find it easier to work with Netscape.
// Perform some processing on a StringBuffer called buffer
catch (Exception e)
System.err.println ("Err- " +e);
System.err.println ("Value of buffer before error " + buffer);
Redirecting standard output and error to another stream
Sometimes you may want to redirect standard output and error to another stream, such as a file. While you can redirect output and error streams from an operating system (i.e., java SomeApplication > output.txt ) for applications, this won't work with applets or servlets. Applets and servlets are still capable of using output and error streams, but the user can't see them. You can assign a new output or error stream by using the System class, which offers two methods for assigning new streams
System.setOut ( PrintStream out )
System.setErr ( PrintStream err )
For example, to redirect standard error to a file, you could use the following statement:
Listing 3. Redirecting standard error to a file.
System.setErr ( new PrintStream ( new FileOutputStream ( "error.txt" ) ) );
Logging to a file
If you prefer a structured approach, you may want to use a log file, rather than displaying error messages and variable state to output streams. The format for them is up to you, but most log files include the time and date of an action or error, as well as a suitable description or error code to allow programmers to diagnose the problem. Log files aren't as useful as trace debugging, or logging of program state, but they are more compact and can help to show the general location of an error. They're also particularly useful for Java servlets, as a servlet that displays an error message to the user through HTML output can leave a permanent record for later access. If logs aren't used, it might become hard to track down the error if the user forgets to save the page contents or error message.
Writing to log files is extremely easy. Simple open the log file in append mode, by using the FileOutputStream (String filename, boolean appendFlag) constructor, and be sure to include the time of the error. For example, to write a message to 'log'.txt, including the time, the following snippet of code might be used.
Listing 4. Writing to a log file.
// Prevent overwriting of previous entries
FileOutputStream fout = new FileOutputStream ( "servlet.log" , true );
PrintStream pout = new PrintStream (fout);
// Log a database access error
pout.println (new java.util.Date() + " - Unable to access database");
Though trace debugging is by far the most efficient means of locating and eradicating bugs, as it provides a visual interface and allows you to selectively step through code or examine the state of variables and objects, many debugging tools fail to support effective applet or servlet debugging. In such situations, you must resort to logging of program state and examining the data displayed. Either way, you'll find these techniques an invaluable aid in removing bugs from your Java applications, applets, and servlets.
About the Author
David Reilly is a software engineer and freelance technical writer living in Australia. A Sun Certified Java 1.1 Programmer, his research interests include the Java programming language, networking & distributed systems, and software agents. He can be reached via e-mail at firstname.lastname@example.org or through his personal site.