Chapter 4. Debugging

Debugger Types

Real programmers know that watching their applications run smoothly isn’t nearly as much fun as just getting them to run in the first place. The best part is seeing what’s going on under the hood. Don’t you just love to watch the threads weaving around, and the values in one variable affecting another like a chain of dominos falling over? Well, the people at NetBeans understand, so they created some great debugging features. In the event that you make a programming error and can’t immediately see what’s wrong, you can use these debugging features to find out what’s really going on. But I’m sure you’re more careful than that, so just think of debugging as a fun diversion from writing code. Here are some of the ways you can play with the inner workings of your programs while they’re running:

  • Set breakpoints

  • Watch values change for variables and expressions

  • Examine and modify variable values

  • Step through code, line by line

  • Examine running threads, classes, and the callstack

  • Run several debugging sessions at the same time

Actually, the same user interface supports a choice of backend debugging facilities. The Java Platform Debugger Architecture (JPDA) is the default. Be sure to specify the right debugger type for the object to be debugged; these types are delineated in Table 4-1.

Table 4-1. Debugger types that come with NetBeans 3.3

Debugger type

Remarks

Where to use it

Default

Based on JPDA

Use with ordinary Java applications

Applet

Based on JPDA

Required with applets

JDK 1.1

Pre-JPDA, fewer features

Use only with pre-Java 2 applications

RMI

Based on JPDA

Use with RMI applications

J2EE server

Based on JPDA

Use with J2EE Server Integration

You can see the various debugger types for NetBeans in Figure 4-1.

Debugger types

Figure 4-1. Debugger types

Breakpoint Types

Originally, setting a breakpoint simply told the debugger to stop execution at a specified source code line and to enter into an interactive debugging mode. But NetBeans (with JPDA) does much more than that. Setting a breakpoint now means responding to a variety of runtime events, listed in Table 4-2. The user can configure the response to each breakpoint to include either or both of the following actions:

  • Suspend debugging pauses execution to allow the user to use the interactive debugging features.

  • Print text displays a fully configurable message in the output window.

Table 4-2. Runtime events that can be used to trigger a breakpoint define the following breakpoint types

Breakpoint type

Runtime event

Options—specify when to activate breakpoint

Line

Execution reaches line in source

Condition—optional boolean expression

Method

Execution enters method

Apply also to anonymous inner classes, or to named classes only

Apply to all methods of given classes, or only selected methods

Condition—optional boolean expression

Exception

Exception is thrown

Exception caught, uncaught, or both

Condition—optional boolean expression

Variable

Variable is accessed or modified

All access to variable, or modification only

Condition—optional boolean expression

Thread

Thread starts or dies

Thread start, thread death, or both

Class

Class is loaded or unloaded

Class loaded, unloaded, or both

The Condition option is probably the most interesting. You may enter a boolean expression as the Condition property in the dialogs for setting Line, Exception, Variable, and Method breakpoints. Start your application in debug mode, and it will ignore the breakpoints until the conditional expression evaluates to true. This saves lots of time, compared to stepping over breakpoints manually until you reach the conditions that you want to investigate.

There are some scope considerations in creating a breakpoint condition. With these considerations in mind, a breakpoint condition can be any valid Java expression that appears on the right side of the equals sign in an assignment statement with a boolean variable on the left side. Additionally:

  • Variables and expressions in the Condition must be valid within the class, method, or line that the breakpoint is set in.

  • “Import” statements are ignored, so external references must be fully qualified (e.g., use java.lang.String, instead of simply String).

  • Outer class methods and variables are not directly accessible, so they also must be fully qualified (e.g., use this.variableName or this$1.variableName).

Adding and Removing Breakpoints

Let’s have some fun and try out some of the debugging features. But first we need something to debug. Here’s a little multithreaded class, shown in Example 4-1. Each thread simply displays a counter while counting to a limit. You may follow along with this example or use one of your own and do the same exercises. If you want to use this one, create a package named Debug under some working directory that you’ve mounted in the NetBeans Explorer. Download the source from the book’s web site, or type it in manually.

Example 4-1. Multithreaded example for debugging demo—ThreadedCounter.java

package Debug;

/**
 * Multithreaded example for debugging demo.
 *
 * @author  vaughn
 */
public class ThreadedCounter {

/** thread sleep delay interval for CountThread */
  private static long delay = 500;

/** number of iterations for 1st instance of CountThread */
  private static int maxcount = 20;

/** inner class object to run as a separate thread */
  private class CountThread implements Runnable {

/** number of iterations for CountThread */
    private int maxcount = 1000;

/** identifying name for instance of CountThread */
    private String countName;

/** iteration counter */
    private int count;

/** CountThread constructor */
    public CountThread(String name, int maxcount) {
      super( );
      this.maxcount = maxcount;
      this.countName = name;
      new Thread(this).start( );
    }

/** run method for thread */
    public void run( ) {
      for (count = 0; count < maxcount; count++) {
          try {
              Thread.sleep(delay);
          } catch (InterruptedException e) {}
          System.out.println(countName + " = " + count);
      }
    }
  }

/** ThreadedCounter constructor */
  public ThreadedCounter(String name, int limit) {
    System.out.println(name + " limit = " + limit);
    CountThread ct = new ThreadedCounter.CountThread(name, limit);
  }

  /**
  * @param args the command line arguments
  */
  public static void main (String args[ ]) {
    new ThreadedCounter("1st", maxcount);
    new ThreadedCounter("2nd", 10);
  }
}

Compile and execute ThreadedCounter, just to see what it does. Not much, but enough for our purposes. Both threads display the current count every half second. The second thread counts from 0 to 9 and then quits, whereas the first thread counts all the way to 19.

Now let’s set some breakpoints. First, set breakpoint type Line at the line that starts with System.out.println in the ThreadedCounter constructor. As usual, there are several ways to do this. Open ThreadedCounter in the Source Editor, and put the cursor on the line with System.out.println. Right-click to get the context menu (shown in Figure 4-2), or open the Debug menu in the main window. Select Toggle Breakpoint from either menu. Or you could just put the cursor on the desired line and press Shift-F8. Notice that the line is highlighted in pink after the breakpoint has been set.

Setting breakpoints

Figure 4-2. Setting breakpoints

To see something a bit more interesting let’s set a breakpoint type Thread. Select Add Breakpoint from the Debug menu, or press Control-Shift-F8 to pop up the Add Breakpoint dialog box (shown in Figure 4-3). Select Breakpoint Type Thread and then change the Set Breakpoint On property to Thread Start or Death.

Add breakpoint dialog

Figure 4-3. Add breakpoint dialog

Finally, let’s set a breakpoint type Variable on the count variable in the CountThread inner class. This breakpoint type might be better named breakpoint type Field, because it only seems to work on variables with classwide scope. This author could not get it to work for variables with a narrower scope, such as internal to a for loop. That’s why count in the code example is declared as a class member, instead of within the for loop. Click anywhere on the private int count; line and then press Control-Shift-F8. When the Add Breakpoint dialog box opens (illustrated in Figure 4-4), make sure Breakpoint Type Variable and Stop On Variable Modification are selected. This time we’ll also set a condition. Any valid boolean expression will work. Enter “count>14.”

Set breakpoint type Variable

Figure 4-4. Set breakpoint type Variable

Setting Watches

Before running the Debugger, let’s set watches on the count and countName variables in the CountThread inner class. A watch gives us a convenient way to see the current value in a variable while the application is running and the value keeps changing. Like setting breakpoints, you could start from the Debug menu or from the Source Editor with the mouse pointer positioned over the variable to be watched. For this example, put the pointer over count anywhere in the source, press Control-Shift-F7, then hit OK in the dialog box that pops up. Repeat with countName. That’s all there is to it. Now, we’re ready to debug.

Starting the Debugger

You can start the Debugger from the Editing, GUI Editing, or Debugging workspace. The source to be debugged must be open and selected in the Explorer or Source Editor, just as if you intended to Execute the class. Select Start from the Debug menu (as shown in Figure 4-5), or press Alt-F5 to launch the Debugger. If you want the Debugger to pause at the first executable statement, press F7 to start it. Once it starts, the IDE switches to the Debugging workspace.

Starting the debugger

Figure 4-5. Starting the debugger

When the Debugger starts, the class being debugged executes normally until it hits a breakpoint. Then it pauses with the Source Editor window open and the breakpoint line highlighted. If the program flow does not hit a breakpoint, you can always seize control by selecting Pause from the Debug menu. In our example we also set a breakpoint on Thread Start or Death, so there will not be a highlighted line at the first few pauses. Just press Control-F5 to continue until it hits a breakpoint line. Then you can use Step Over, Step Into, and other standard debugger features to walk through the source. Table 4-3 is a summary of features available through the Debug menu.

Table 4-3. Debug menu options

Debug menu

Shortcut keys

Debugger action

Start

Alt-F5

Start Debugger session and switch to Debugger workspace.

Finish

Shift-F5

End Debugger session.

Attach

Debug a remote process.

Continue

Control-F5

Resume execution until the next breakpoint.

Pause

Pause a running program that has not hit a breakpoint.

Run to Cursor

F4

Resume execution until the program pointer reaches the location of the cursor in the source.

Step Over

F8

Execute to next line in current method or constructor.

Step Into

F7

Same as Step Over (F8), unless current line is a method call. In that case, pauses at the first executable line in the method. This works to start a new debugger session and pause at the first line of the main method.

Step Out

Alt-Shift-F7

Same as Step Over (F8), unless current line is in a method. In that case, executes normally through return and pauses in the calling source at the line after the method call.

Go to Called Method

Control-Alt-Up

Move cursor in source to the current execution line in a called method. This only affects the cursor location, not the flow of execution.

Go to Calling Method

Control-Alt-Down

Move cursor in source to the point from which the current method was called. This only affects the cursor location, not the flow of execution.

Toggle Breakpoint

Shift-F8

If the cursor is on a line that does not have a breakpoint set, set a breakpoint. Otherwise, clear the breakpoint.

Add Breakpoint

Control-Shift-F8

Open dialog to add several breakpoint types—Line, Method, Exception, Variable, Thread, Class.

Add Watch

Control-Shift-F7

Add a watch on a variable or expression.

Debugger Window

Control-5

Open the Debugger window. (See next section for details.)

The Debugger Window

Remember my remark at the beginning of this chapter that the best part of programming is seeing what’s going on under the hood? That’s why the NetBeans people gave us the Debugger Window. So start a debugging session, let it switch you to the Debugging Workspace, and we’ll look at what you can do with the Debugger Window. If the Debugger Window doesn’t open automatically, press Control-5 to open it. This window is shown in Figure 4-6.

Debugger window

Figure 4-6. Debugger window

The Debugger Window has three sections:

Toolbar

Buttons to toggle View Panels and Property Sheet off and on

View Panels

Up to seven views of what’s going on under the hood of your programs

Property Sheet

Property info on an object selected on a View Panel

The View Panels, which allow you to examine in depth the objects involved in running the object being debugged, include the following:

Sessions

Shows all active debugging sessions. You can launch any number of simultaneous debugging sessions and switch back and forth among them.

Breakpoints

Shows all breakpoints of every type set in all active sessions

Threads

Shows all live threads in all active sessions

Call Stack

Shows stack trace for each live thread in the currently selected session

Watches

Shows all watches set on variables in all active sessions

Variables

Shows all variables visible within scope of currently selected thread

Classes

Shows classes visible in currently selected session

Use the debug functions for Continue, Step Into, and Step Over to slowly execute our ThreadedCounter example. Notice the values changing in the watches on the count and countName variables. Separately, the Output Window tracks execution progress by logging breakpoints that are hit. Open up nodes in the panels to look as deeply as you like. Inspecting the objects while they are running reveals what the JVM is doing with the classes that you’re debugging.

The Variables View

Let’s investigate the Variables view to see what’s possible. You should have a Debugger Session running with ThreadedCounter, and the Debugger Window should be open with the Variables panel and the Property Sheet visible. If necessary, click the appropriate button in the Tool bar to make the panels visible.

Press Control-F5 a few times to get past startup and into the main execution phase. Continue until the Watches or Variables panel shows count = 3. Click the value for count, click again to edit it, and change the value to count = 6. That’s how easy it is to examine and modify the value of a variable during debugging. Try it with countName. Change the value to “3rd”, or whatever you like. Or, as shown in Figure 4-7, open the nodes to expose the individual characters, and change them one by one.

Modifying a variable

Figure 4-7. Modifying a variable

Remote Debugging in NetBeans

Any Java executable that can be run from the IDE can be debugged by it. This gives a developer the full range of NetBeans’ debugging power for working with standalone applications, applets, and client applications that are based on a variety of technologies. The NetBeans debugger can also attach to a Java process already running in a separate JVM, either on the same computer or on a remote machine. This gives a developer the same power for debugging remote applications and components running in Java-based server containers.

Attaching to a remote JVM makes it possible to use breakpoints, conditionals, watches, and other debugging features with servlets, Enterprise JavaBeans, and RMI or CORBA server objects. Debugging remote objects without such features requires inserting a myriad of System.out.println( ) statements or other logging code. Using logging code for debugging is slow and inefficient, whereas using NetBeans is much more powerful.

Debugging Remotely

  1. Start the JVM that you want to debug with the following switches:

    java -Xint -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,
      address=12999,suspend=n ...
    -Xint

    Turn off Hotspot optimizations. Although this is not strictly necessary, it does make the JVM far more stable while debugging.

    -Xdebug

    Turn on debug mode.

    -Xnoagent

    Disable the old sun.tools.debug.

    -Xrunjdwp

    Specify connection details:

    • transport=dt_socket: tell the JVM to allow a remote debugger to connect via socket. Alternatively, transport=dt_shmem allows the remote debugger to connect over shared memory, which requires that the debugger be on the same machine as the JVM. As of NetBeans 3.2.1, it seems that only dt_socket is supported.

    • server=y: tell the JVM to wait for a remote debugger to attach to it. Alternatively, server=n would cause the JVM to try to attach to a remote debugger.

    • address=12999: specify port number to listen on. This can be any port that you have permissions to. Remember this port because you will need it to configure NetBeans later.

    • suspend=n: if this is “y,” it causes the JVM to suspend before the main class is loaded.

  2. Start NetBeans.

  3. Select Debug Attach from the menus.

  4. In the Attach window (shown in Figure 4-8) select the Default Debugger, the host you are connecting to, and the port on that machine that you specified when you started the JVM.

    The Attach dialog box

    Figure 4-8. The Attach dialog box

  5. In your NetBeans output window, you will see Connecting to localhost:12999. You will be able to see your connection in the Sessions tab of the Debugger Window, shown in Figure 4-9.

    The Debugger window

    Figure 4-9. The Debugger window

  6. Use the NetBeans Debugger just as you would a local debugging session.

Caveats

Connecting to a separate JVM, especially across a network to a remote machine, is significantly more complex behind the scenes and has more possibilities for problems. Nevertheless, the advanced debugging features more than make up for any extra patience and effort that the following issues may require.

  • If you attach to debug a JVM and then disconnect, you will not be able to reconnect unless you restart your JVM. It seems that a given JVM will only accept one debugger connection while it is running.

  • In Debug mode, your JVM may be much slower, because it needs to send a record of all method calls to the debugger.

  • In Debug mode, your JVM will also be very unstable. Make sure that you have -Xint specified to disable Hotspot optimizations. You may also want to add -Djava.compiler=NONE to turn off the Just-In-Time (JIT) compiler.

  • Conditional breakpoints with complex conditions may not work.

Advanced Features

If you have the source code of your server software, you can even debug into the server code. For example, open source projects such as JBoss, an Enterprise JavaBeans Container (found at http://www.jboss.org), and Tomcat, a Java Servlet engine (found at http://jakarta.apache.org), provide you with the source code of their servers. If you mount that code as a filesystem in NetBeans, you can debug into that code as well (you need not compile the code for this to work). Note that the version of the code must match exactly with that of the running software (i.e., trying to debug JBoss 2.4.0 with the source from JBoss 2.4.1 will not work).

Now that you have the tools for getting out of trouble, you’re ready for more interesting ways to get into trouble. The chapters ahead will give you ample opportunity to get well acquainted with the NetBeans debugger as we look at increasingly sophisticated tools and technologies for building Java applications and components with NetBeans.

Get NetBeans: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.