Tutorial 18 - Debugging
Bugs or programming errors often occur when learning a new programming language or when developing a new routine or utility. This tutorial will give some successful strategies for squashing any bugs that may occur.
Well documented code assists in faster debugging. Javadoc aids in this documentation task.
Types of Errors
There are several types of error, each with its own method of detection and repair. The basic types of error are:
- Syntax errors are errors in grammar and punctuation such as mismatched quotes, missed commas or case-sensitivity issues. These errors are caught during the compilation process.
- Runtime errors only show up as the program is executed. A common example is division by zero. These can be caught by static testing.
- Logic errors are basic errors in the programmer's algorithms or procedural errors. Diagnosis only comes when incorrect results occur and solution requires mapping out the flow for test cases. An example is wrong scoping of a variable. Dynamic testing is required.
- Incorrect operator precedence errors are basic mathematical grouping errors. The best way to avoid them is with brackets to force the grouping explicitly. Static testing will show some types of problem. Dynamic testing will catch others.
- Threading errors can occur when multiple threads are programmed. Common issues are access, deadlock, data race and synchronization. These types of errors are very difficult to trace.
One type of compiler warning message that should be corrected A.S.A.P. is a deprecation warning. This occurs when an out of date method is used. Find out what the new technique is and replace older code with it. Check source libraries for other programs with the same construct.
Common Errors
Errors can take some of the fun out of programming but experience will gradually eliminate (or at least make you more watchful for) certain commonly made ones. Some of these are:
- Spelling and typos. Remember case sensitivity and avoid hard to find lookalikes (1 and l, 0 and O, etc.).
- Brackets or quotes not matched.
- Wrong type of bracket. Each has its own use.
- Not using the escaper backslash when required.
- Uninitialized variables may be referenced accidentally.
- Arrays have bounds. Don't stray outside them.
- The type of the variable may be critical.
- Casting a variable can cause loss of accuracy.
- The scope of a variable can affect its current value.
Testing Methods
Brute force tests include comments, println(), stack traces and using the exception class. The time-proven first attack is to insert println() statements and/or manual breakpoints (ie. pauses) where problems are suspected and then running the program. Dump and analyze a list of values by redirecting the output stream to a file or by using Throwable().printStackTrace and Thread.currentThread.dumpStack. Use System.currentTimeMillis() [returns long] for timing checks.
Static tests are used to reduce known causes of programming problems. They use lint-like tools to spot run-time errors and poor practice such as variable use before definition, conditions that are constant, loop range problems, etc. Some static testers such as Hammurapi analyze at the source level but watch out for those tools that are style-dependent unless you use that specific style. Others such as FindBugs analyze class (bytecoded) files. PMD is interesting as it can be customized for 'rules to be tested' and can also be integrated into many of the IDEs and text editors (such as TextPad). Wikipedia and google searching give more examples of static testers. I use antiC (source files) and JLint (class files). to check my tutorial material.
Dynamic tests are used to check logic and regression (worked ok in last version) errors. Debuggers are frequently used. A systematic unit test approach is highly recommended. This involves writing test drivers and rerunnable scripts to specifically test each method TWICE; once with valid data and once with invalid data. jUnit provides a test frame and many IDEs like DrJava already have jUnit included.
Assertions pretest conditions prior to method invocation. They are used to avoid runtime errors caused by bad assumptions. Java uses assert <boolean expression or method> to test the validity of a logic statement such as assert a<MAX. If the assertion is not true, an AssertionError exception is thrown. assert a<MAX : "Maximum has been exceeded" will attach a custom error message to the exception for display purposes. Common uses of assertions are to check for null strings passed as method parameters and to prevent division by zero.
NOTE: To enable assertion testing at runtime use a -ea switch on the command line. If you are using an IDE, you must RTFM.
Reflection is the capability for one object to inspect another object's characteristics. It uses classes provided by the java.lang.reflect package. These classes are Array, Constructor, Field, Method, Modifier, AccessibleObject and ReflectPermission. A useful example of reflection is provided by SeeMethods.java (found in jp2string.zip). After compilation java SeeMethods classname will give a display of all methods in classname as well as their modifiers, return type and parameters. Reflection can help when brute-force testing.
Debugging Utilities
Logic errors and run-time errors are difficult to track down. Most operating systems have a debugger utility to trace through an executing program and to inspect certain conditions dynamically. Common functions of a debugger are:
- Maintaining source code line information so user can trace to a specific line or construct.
- Allowing inspection of variables within the current scope.
- Allowing inspection of a specific thread.
- Allowing single stepping through statements, lines or methods.
- Allowing setting of break points (ie. program will run normally but halt for inspection at this point).
- Retrieving some operating parameters.
- Proving a stack trace.
In addition, Java debuggers have the following needs:
- The interface must be object oriented.
- Runtime features like threads and monitors must be supported.
- Remote debugging of applications and applets should be possible.
- Security must not be compromised.
Java JDK includes the Java Platform Debugger Architecture and the the jdb command line debugger for squashing bugs in class logic. Run the utility by entering jdb classname at the system prompt. Typing help at the jdb prompt will give a list of valid commands. Oracle gives the definitive explanation of jdb.
Although jdb is a good command line debugger, many users have come to rely on graphical interfaces for their work. Several OpenSource examples are:
| jDebugTool | jSwat [GPL] | Omniscient Debugger [gnu] | javadt [sun] |
Many IDEs (Interactive Development Environments) provide their own graphical debuggers. These include DrJava (Rice University), Studio (Sun), JBuilder (Borland) and JDeveloper (Oracle). A great tutorial on using IDE debuggers is offered by DigiLife.
Setting Breakpoints
Breakpoints are temporary markers placed in a program to tell the debugger where to stop program execution. Setting breakpoints can help isolate problems in statement logic. For example, if a particular statement is causing problems, set a breakpoint on the line containing the statement, then run the program. Execution stops at the breakpoint before the statement is executed. Check the contents of variables, registers, storage and the stack, then step over (or execute) the statement to see how the problem arises.
Various debuggers support several types of breakpoints. Some of the most common are:
- Line breakpoints are triggered before the code at a particular line in a program is executed.
- Method breakpoints are triggered when a method that has been set as a breakpoint is reached.
- Counter breakpoints are triggered when a counter assumes or goes beyond a particular value.
- Exception breakpoints are triggered when code throws a particular type of exception.
- Storage change breakpoints are triggered when the storage within a particular storage address range is changed.
- Address breakpoints are triggered when the address a breakpoint is set for has been reached.
Note: Some debuggers only support certain types of breakpoints in compiled versions of Java code (using a just-in-time compiler) and not within interpreted code (code that is compiled by the javac tool). An example is the Address breakpoint. Each tool is slightly different in the way breakpoints are set. Check the documentation.
Using Javadoc
Debugging is greatly assisted by well documented source code. Java includes a documenting utility called javadoc and a standard javadoc style to ensure a well documented program. To prepare source files for javadoc, first make documenting comments into the form of /** comment */ (or **/ for symmetry). The keywords or document meta tags for javadoc (in order of use) are: @param, @return, @exception (or @throws), @author, @version, @see, @since, @serial, @serialData, @serialField, @deprecated and [@link]. These are followed by an appropriate description. Document meta tags can be used for classes, interfaces, fields, constructors and methods. They must immediately precede the item being documented. Each type of comment will be included in all appropriate places in the javadoc documentation notes.
| QUIRK #1: Although not required by Java, the package directive must precede any javadoc comment. |
| QUIRK #2: javadoc does not recognize new lines. They can
be forced with the HTML <br/> element. |
| QUIRK #3: Do not use HTML heading elements! They
interfere with the look of javadoc notes. Any other HTML element is ok. |
To generate the javadoc documentation first create a docs subfolder in your project folder. Next from a DOS window run the command javadoc -d docs -author *.java. This assumes that you have correctly set the path to the java /bin folder.
To view your javadoc html documentation open the docs folder and click on index.html.