Using a Debugger
Debugging with print statements is not one of life's more joyful experiences. You constantly find yourself adding and removing the statements, then recompiling the program. Using a debugger is better. A debugger runs your program in full motion until it reaches a breakpoint, and then you can look at everything that interests you.
The JDB Debugger
The JDK includes JDB, an extremely rudimentary command-line debugger. Its user interface is so minimal that you will not want to use it except as a last resort. It really is more a proof of concept than a usable tool. We nevertheless briefly introduce it because there are situations in which it is better than no debugger at all. Of course, many Java programming environments have far more convenient debuggers. The main principles of all debuggers are the same, and you may want to use the example in this section to learn to use the debugger in your environment instead of JDB.
Examples 11-9 through 11-11 show a deliberately corrupted version of the ButtonTest program from Chapter 8. (We broke up the program and placed each class into a separate file because some debuggers have trouble dealing with multiple classes in the same file.)
When you click on any of the buttons, nothing happens. Look at the source codebutton clicks are supposed to set the background color to the color specified by the button name.
Example 11-9. BuggyButtonTest.java
1. import javax.swing.*; 2. 3. public class BuggyButtonTest 4. { 5. public static void main(String[] args) 6. { 7. BuggyButtonFrame frame = new BuggyButtonFrame(); 8. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 9. frame.setVisible(true); 10. } 11. }
Example 11-10. BuggyButtonFrame.java
1. import java.awt.*; 2. import javax.swing.*; 3. 4. public class BuggyButtonFrame extends JFrame 5. { 6. public BuggyButtonFrame() 7. { 8. setTitle("BuggyButtonTest"); 9. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 10. 11. // add panel to frame 12. 13. BuggyButtonPanel panel = new BuggyButtonPanel(); 14. add(panel); 15. } 16. 17. public static final int DEFAULT_WIDTH = 300; 18. public static final int DEFAULT_HEIGHT = 200; 19. }
Example 11-11. BuggyButtonPanel.java
1. import java.awt.*; 2. import java.awt.event.*; 3. import javax.swing.*; 4. 5. class BuggyButtonPanel extends JPanel 6. { 7. public BuggyButtonPanel() 8. { 9. ActionListener listener = new ButtonListener(); 10. 11. JButton yellowButton = new JButton("Yellow"); 12. add(yellowButton); 13. yellowButton.addActionListener(listener); 14. 15. JButton blueButton = new JButton("Blue"); 16. add(blueButton); 17. blueButton.addActionListener(listener); 18. 19. JButton redButton = new JButton("Red"); 20. add(redButton); 21. redButton.addActionListener(listener); 22. } 23. 24. private class ButtonListener implements ActionListener 25. { 26. public void actionPerformed(ActionEvent event) 27. { 28. String arg = event.getActionCommand(); 29. if (arg.equals("yellow")) 30. setBackground(Color.yellow); 31. else if (arg.equals("blue")) 32. setBackground(Color.blue); 33. else if (arg.equals("red")) 34. setBackground(Color.red); 35. } 36. } 37. }
In a program this short, you may be able to find the bug just by reading the source code. Let us pretend that scanning the source code for errors is not practical. Here is how you can run the debugger to locate the error.
To use JDB, you must first compile your program with the -g option, for example:
javac -g BuggyButtonTest.java BuggyButtonFrame.java BuggyButtonPanel.java
When you compile with this option, the compiler adds the names of local variables and other debugging information into the class files. Then you launch the debugger:
jdb BuggyButtonTest
Once you launch the debugger, you will see a display that looks something like this:
Initializing jdb... >
The > prompt indicates the debugger is waiting for a command. Table 11-4 shows all the debugger commands. Items enclosed in [...] are optional, and the suffix (s) means that you can supply one or more arguments separated by spaces.
Table 11-4. Debugging Commands
threads [threadgroup] |
Lists threads |
thread thread_id |
Sets default thread |
suspend [thread_id(s)] |
Suspends threads (default: all) |
resume [thread_id(s)] |
Resumes threads (default: all) |
where [thread_id] or all |
Dumps a thread's stack |
wherei [thread_id] or all |
Dumps a thread's stack and program counter info |
threadgroups |
Lists thread groups |
threadgroup name |
Sets current thread group |
print name(s) |
Prints object or field |
dump name(s) |
Prints all object information |
locals |
Prints all current local variables |
classes |
Lists currently known classes |
methods class |
Lists a class's methods |
stop in class.method |
Sets a breakpoint in a method |
stop at class:line |
Sets a breakpoint at a line |
up [n] |
Moves up a thread's stack |
down [n] |
Moves down a thread's stack |
clear class:line |
Clears a breakpoint |
step |
Executes the current line, stepping inside calls |
stepi |
Executes the current instruction |
step up |
Executes until the end of the current method |
next |
Executes the current line, stepping over calls |
cont |
Continues execution from breakpoint |
catch class |
Breaks for the specified exception |
ignore class |
Ignores the specified exception |
list [line] |
Prints source code |
use [path] |
Displays or changes the source path |
memory |
Reports memory usage |
gc |
Frees unused objects |
load class |
Loads Java class to be debugged |
run [class [args]] |
Starts execution of a loaded Java class |
!! |
Repeats last command |
help (or ?) |
Lists commands |
exit (or quit) |
Exits debugger |
We cover only the most useful JDB commands in this section. The basic idea, though, is simple: you set one or more breakpoints, then run the program. When the program reaches one of the breakpoints you set, it stops. You can inspect the values of the local variables to see if they are what they are supposed to be.
To set a breakpoint, use
stop in class.method
or the command
stop at class:line
For example, let us set a breakpoint in the actionPerformed method of BuggyButtonTest. To do this, enter
stop in BuggyButtonPanel$ButtonListener.actionPerformed
Now we want to run the program up to the breakpoint, so enter
run
The program will run, but the breakpoint won't be hit until Java starts processing code in the actionPerformed method. For this, click on the Yellow button. The debugger breaks at the start of the actionPerformed method. You'll see:
Breakpoint hit: thread="AWT-EventQueue-0", BuggyButtonPanel$ButtonListener.actionPerformed (), line=28, bci=0 28 String arg = event.getActionCommand();
The list command lets you find out where you are. The debugger will show you the current line and a few lines above and below. You also see the line numbers. For example:
24 private class ButtonListener implements ActionListener 25 { 26 public void actionPerformed(ActionEvent event) 27 { 28=> String arg = event.getActionCommand(); 29 if (arg.equals("yellow")) 30 setBackground(Color.yellow); 31 else if (arg.equals("blue")) 32 setBackground(Color.blue); 33 else if (arg.equals("red"))
Type locals to see all local variables. For example,
Method arguments: event = instance of java.awt.event.ActionEvent(id=698) Local variables:
For more detail, use
dump variable
For example,
dump event
displays all instance fields of the evt variable.
event = instance of java.awt.event.ActionEvent(id=698) { SHIFT_MASK: 1 CTRL_MASK: 2 META_MASK: 4 ALT_MASK: 8 ACTION_FIRST: 1001 ACTION_LAST: 1001 ACTION_PERFORMED: 1001 actionCommand: "Yellow" modifiers: 0 serialVersionUID: -7671078796273832149 . . .
There are two basic commands to single-step through a program. The step command steps into every method call. The next command goes to the next line without stepping inside any further method calls. Type next twice and then type list to see where you are.
The program stops in line 31.
27 { 28 String arg = event.getActionCommand(); 29 if (arg.equals("yellow")) 30 setBackground(Color.yellow); 31=> else if (arg.equals("blue")) 32 setBackground(Color.blue); 33 else if (arg.equals("red")) 34 setBackground(Color.red); 35 }
That is not what should have happened. The program was supposed to call setColor(Color.yellow) and then exit the method.
Dump the arg variable.
arg = "Yellow"
Now you can see what happened. The value of arg was "Yellow", with an uppercase Y, but the comparison tested
if (arg.equals("yellow"))
with a lowercase y. Mystery solved.
To quit the debugger, type:
quit
As you can see from this example, the jdb debugger can be used to find an error, but the command-line interface is very inconvenient. Remember to use list and locals whenever you are confused about where you are. But if you have any choice at all, use a better debugger for serious debugging work.
The Eclipse Debugger
Eclipse has a modern and convenient debugger that has many of the amenities that you would expect. In particular, you can set breakpoints, inspect variables, and single-step through a program.
To set a breakpoint, move the cursor to the desired line and select Run -> Toggle Line Breakpoint from the menu. The breakpoint line is highlighted (see Figure 11-8).
Figure 11-8 Breakpoints in the Eclipse debugger
To start debugging, select Run -> Debug As -> Java Application from the menu. The program starts running. Set a breakpoint in the first line of the actionPerformed method.
When the debugger stops at a breakpoint, you can see the call stack and the local variables (see Figure 11-9).
Figure 11-9 Stopping at a breakpoint
To single-step through the application, use the Run -> Step into (F5) or Run -> Step over (F6) commands. In our example, press the F6 key twice to see how the program skips over the setBackground(Color.yellow) command. Then watch the value of arg to see the reason (see Figure 11-10).
Figure 11-10 Inspecting variables
As you can see, the Eclipse debugger is much easier to use than JDB because you have visual feedback to indicate where you are in the program. Setting breakpoints and inspecting variables is also much easier. This is typical of debuggers that are a part of an integrated development environment.
This chapter introduced you to exception handling and gave you some useful hints for testing and debugging. The next chapter covers streams.