Debugging Techniques

   


Suppose you wrote your program and made it bulletproof by catching and properly handling all exceptions. Then you run it, and it does not work right. Now what? (If you never have this problem, you can skip the remainder of this chapter.)

Of course, it is best if you have a convenient and powerful debugger. Debuggers are available as a part of professional development environments such as Eclipse, NetBeans, or JBuilder. However, if you use a new version of Java that is not yet supported by development environments or if you work on an unusual platform, you will need to do a great deal of debugging by the time-honored method of inserting logging statements into your code.

Useful Tricks for Debugging

Here are some tips for efficient debugging if you have to do it all yourself.

  1. You can print or log the value of any variable with code like this:

     System.out.println("x=" + x); 

    or

     Logger.global.info("x=" + x); 

    If x is a number, it is converted to its string equivalent. If x is an object, then Java calls its toString method. To get the state of the implicit parameter object, print the state of the this object.

     Logger.global.info("this=" + this); 

    Most of the classes in the Java library are very conscientious about overriding the toString method to give you useful information about the class. This is a real boon for debugging. You should make the same effort in your classes.

  2. One seemingly little-known but very useful trick is that you can put a separate main method in each class. Inside it, you can put a unit test stub that lets you test the class in isolation.


    public class MyClass
    {
       methods and fields
       . . .
       public static void main(String[] args)
       {
          test code
       }
    }

    Make a few objects, call all methods, and check that each of them does the right thing. You can leave all these main methods in place and launch the Java virtual machine separately on each of the files to run the tests. When you run an applet, none of these main methods are ever called. When you run an application, the Java virtual machine calls only the main method of the startup class.

  3. If you liked the preceding tip, you should check out JUnit from http://junit.org. JUnit is a very popular unit testing framework that makes it easy to organize suites of test cases. Run the tests whenever you make changes to a class, and add another test case whenever you find a bug.

  4. A logging proxy is an object of a subclass that intercepts method calls, logs them, and then calls the superclass. For example, if you have trouble with the setBackground method of a panel, you can create a proxy object as an instance of an anonymous subclass:

     JPanel panel = new    JPanel()    {       public void setBackground(Color c)       {          Logger.global.info("setBackground: c=" + c);          super.setBackground(c);       }    }; 

    Whenever the setBackground method is called, a log message is generated. To find out who called the method, generate a stack trace see the next tip.

  5. You can get a stack trace from any exception object with the printStackTrace method in the Throwable class. The following code catches any exception, prints the exception object and the stack trace, and rethrows the exception so it can find its intended handler.

     try {    . . . } catch (Throwable t) {    t.printStackTrace();    throw t; } 

    You don't even need to catch an exception to generate a stack trace. Simply insert the statement

     Thread.dumpStack(); 

    anywhere into your code to get a stack trace.

  6. Normally, the stack trace is displayed on System.err. You can send it to a file with the void printStackTrace(PrintWriter s) method. Or, if you want to log or display the stack trace, here is how you can capture it into a string:

     StringWriter out = new StringWriter(); new Throwable().printStackTrace(new PrintWriter(out)); String trace = out.toString(); 

    (See Chapter 12 for the PrintWriter and StringWriter classes.)

  7. It is often handy to trap program errors in a file. However, errors are sent to System.err, not System.out. Therefore, you cannot simply trap them by running

     java MyProgram > errors.txt 

    In UNIX and Windows NT/2000/XP, this is not a problem. For example, if you use bash as your shell, simply capture the error stream as

     java MyProgram 2> errors.txt 

    To capture both System.err and System.out in the same file, use

     java MyProgram 2>&1 errors.txt 

    Some operating systems (such as Windows 95/98/Me) do not have such a convenient method. Here is a remedy. Use the following Java program:

     import java.io.*; public class Errout {    public static void main(String[] args) throws IOException    {       Process p = Runtime.getRuntime().exec(args);       BufferedReader err = new BufferedReader(new InputStreamReader(p.getErrorStream()));       String line;       while ((line = err.readLine()) != null)          System.out.println(line);    } } 

    Then run your program as

     java Errout java MyProgram > errors.txt 

    C++ NOTE

    A more efficient way of getting the same result in Windows is to compile this C program into a file, errout.exe:

     #include <io.h> #include <stdio.h> #include <process.h> int main(int argc, char* argv[]) {    dup2(1, 2); /* make stderr go to stdout */    execvp(argv[1], argv + 1);    return 0; } 

    Then you can run

     errout java MyProgram > errors.txt 


  8. To watch class loading, launch the Java virtual machine with the -verbose flag. You get a printout such as:

     [Opened /usr/local/jdk5.0/jre/lib/rt.jar] [Opened /usr/local/jdk5.0/jre/lib/jsse.jar] [Opened /usr/local/jdk5.0/jre/lib/jce.jar] [Opened /usr/local/jdk5.0/jre/lib/charsets.jar] [Loaded java.lang.Object from shared objects file] [Loaded java.io.Serializable from shared objects file] [Loaded java.lang.Comparable from shared objects file] [Loaded java.lang.CharSequence from shared objects file] [Loaded java.lang.String from shared objects file] [Loaded java.lang.reflect.GenericDeclaration from shared objects file] [Loaded java.lang.reflect.Type from shared objects file] [Loaded java.lang.reflect.AnnotatedElement from shared objects file] [Loaded java.lang.Class from shared objects file] [Loaded java.lang.Cloneable from shared objects file] ... 

    This can occasionally be helpful to diagnose class path problems.

  9. If you ever looked at a Swing window and wondered how its designer managed to get all the components to line up so nicely, you can spy on the contents. Press CTRL+SHIFT+F1, and you get a printout of all components in the hierarchy:

     FontDialog[frame0,0,0,300x200,layout=java.awt.BorderLayout,...   javax.swing.JRootPane[,4,23,292x173,layout=javax.swing.JRootPane$RootLayout,...    javax.swing.JPanel[null.glassPane,0,0,292x173,hidden,layout=java.awt.FlowLayout,...    javax.swing.JLayeredPane[null.layeredPane,0,0,292x173,...      javax.swing.JPanel[null.contentPane,0,0,292x173,layout=java.awt.GridBagLayout,...        javax.swing.JList[,0,0,73x152,alignmentX=null,alignmentY=null,...          javax.swing.CellRendererPane[,0,0,0x0,hidden]           javax.swing.DefaultListCellRenderer$UIResource[,-73,-19,0x0,...        javax.swing.JCheckBox[,157,13,50x25,layout=javax.swing.OverlayLayout,...        javax.swing.JCheckBox[,156,65,52x25,layout=javax.swing.OverlayLayout,...        javax.swing.JLabel[,114,119,30x17,alignmentX=0.0,alignmentY=null,...        javax.swing.JTextField[,186,117,105x21,alignmentX=null,alignmentY=null,...        javax.swing.JTextField[,0,152,291x21,alignmentX=null,alignmentY=null,... 

  10. If you design your own custom Swing component and it doesn't seem to be displayed correctly, you'll really love the Swing graphics debugger. And even if you don't write your own component classes, it is instructive and fun to see exactly how the contents of a

    component are drawn. To turn on debugging for a Swing component, use the setDebugGraphicsOptions method of the JComponent class. The following options are available:

    DebugGraphics.FLASH_OPTION

    Flashes each line, rectangle, and text in red before drawing it

    DebugGraphics.LOG_OPTION

    Prints a message for each drawing operation

    DebugGraphics.BUFFERED_OPTION

    Displays the operations that are performed on the off-screen buffer

    DebugGraphics.NONE_OPTION

    Turns graphics debugging off


    We have found that for the flash option to work, you must disable "double buffering," the strategy used by Swing to reduce flicker when updating a window. The magic incantation for turning on the flash option is:

     RepaintManager.currentManager(getRootPane()).setDoubleBufferingEnabled(false); ((JComponent) getContentPane()).setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION); 

    Simply place these lines at the end of your frame constructor. When the program runs, you will see the content pane filled in slow motion. Or, for more localized debugging, just call setDebugGraphicsOptions for a single component. Control freaks can set the duration, count, and color of the flashes see the on-line documentation of the DebugGraphics class for details.

  11. JDK 5.0 adds the -Xlint option to the compiler for spotting common code problems. For example, if you compile with the command

     javac -Xlint:fallthrough 

    then the compiler reports missing break statements in switch statements. (The term "lint" originally described a tool for locating potential problems in C programs, and is now generically applied to tools that flag constructs that are questionable but not illegal.)

    The following options are available:

    -Xlint or -Xlint:all

    Carries out all checks

    -Xlint:deprecation

    Same as -deprecation, checks for deprecated methods

    -Xlint:fallthrough

    Checks for missing break statements in switch statements

    -Xlint:finally

    Warns about finally clauses that cannot complete normally

    -Xlint:none

    Carries out none of the checks

    -Xlint:path

    Checks that all directories on the class path and source path exist

    -Xlint:serial

    Warns about serializable classes without serialVersionUID (see Chapter 12)

    -Xlint:unchecked

    Warns of unsafe conversions between generic and raw types (see Chapter 13)


  12. JDK 5.0 adds support for monitoring and management of Java applications, allowing the installation of agents in the virtual machine that track memory consumption, thread usage, class loading, and so on. This feature is particularly important for large and long-running Java programs such as application servers. As a demonstration of these capabilities, the JDK ships with a graphical tool called jconsole that displays statistics about the performance of a virtual machine (see Figure 11-4). To enable monitoring, start the virtual machine with the -Dcom.sun.management.jmxremote option. Then find out the ID of the operating system process that runs the virtual machine. In UNIX/Linux, run the ps utility; in Windows, use the task manager. Then launch the jconsole program:


    java -Dcom.sun.management.jmxremote MyProgram.java
    jconsole processID

    Figure 11-4. The jconsole Program


  13. If you launch the Java virtual machine with the -Xprof flag, it runs a rudimentary profiler that keeps track of the methods in your code that were executed most often. The profiling information is sent to System.out. The output also tells you which methods were compiled by the just-in-time compiler.

CAUTION

The -X options of the compiler are not officially supported and may not be present in all versions of the JDK. Run java -X to get a listing of all nonstandard options.


Using a Console Window

If you run an applet inside a browser, you may not be able to see any messages that are sent to System.out. Most browsers will have some sort of Java Console window. (Check the help system for your browser.) For example, Netscape Navigator has one, as does Internet Explorer 4 and above. If you use the Java Plug-in, check the Show Java Console box in the configuration panel (see Chapter 10).

Moreover, the Java Console window has a set of scrollbars, so you can retrieve messages that have scrolled off the window. Windows users will find this a definite advantage over the DOS shell window in which the System.out output normally appears.

We give you a similar window class so you can enjoy the same benefit of seeing your debugging messages in a window when debugging a program. Figure 11-5 shows our ConsoleWindow class in action.

Figure 11-5. The console window


The class is easy to use. Simply call:

 ConsoleWindow.init() 

Then print to System.out or System.err in the normal way.

Example 11-5 lists the code for the ConsoleWindow class. As you can see, the class is quite simple. Messages are displayed in a JTextArea inside a JScrollPane. We call the System.setOut and System.setErr methods to set the output and error streams to a special stream that adds all messages to the text area.

Example 11-5. ConsoleWindow.java
  1. import java.awt.*;  2. import java.awt.event.*;  3. import javax.swing.*;  4. import java.io.*;  5.  6. /**  7.    A window that displays the bytes sent to System.out  8.    and System.err  9. */ 10. public class ConsoleWindow 11. { 12.    public static void init() 13.    { 14.       JFrame frame = new JFrame(); 15.       frame.setTitle("ConsoleWindow"); 16.       final JTextArea output = new JTextArea(); 17.       output.setEditable(false); 18.       frame.add(new JScrollPane(output)); 19.       frame.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 20.       frame.setLocation(DEFAULT_LEFT, DEFAULT_TOP); 21.       frame.setFocusableWindowState(false); 22.       frame.setVisible(true); 23. 24.       // define a PrintStream that sends its bytes to the 25.       // output text area 26.       PrintStream consoleStream = new PrintStream(new 27.          OutputStream() 28.          { 29.             public void write(int b) {} // never called 30.             public void write(byte[] b, int off, int len) 31.             { 32.                output.append(new String(b, off, len)); 33.             } 34.          }); 35. 36.       // set both System.out and System.err to that stream 37.       System.setOut(consoleStream); 38.       System.setErr(consoleStream); 39.    } 40. 41.    public static final int DEFAULT_WIDTH = 300; 42.    public static final int DEFAULT_HEIGHT = 200; 43.    public static final int DEFAULT_LEFT = 200; 44.    public static final int DEFAULT_TOP = 200; 45. } 

Tracing AWT Events

When you write a fancy user interface in Java, you need to know what events AWT sends to what components. Unfortunately, the AWT documentation is somewhat sketchy in this regard. For example, suppose you want to show hints in the status line when the user moves the mouse over different parts of the screen. The AWT generates mouse and focus events that you may be able to trap.

We give you a useful Eventtrace class to spy on these events. It prints out all event handling methods and their parameters. See Figure 11-6 for a display of the traced events.

Figure 11-6. The Eventtracer class at work


To spy on messages, add the component whose events you want to trace to an event tracer:

 EventTracer tracer = new EventTracer(); tracer.add(frame); 

That prints a textual description of all events, like this:

public abstract void java.awt.event.MouseListener.mouseExited(java.awt.event.MouseEvent): java.awt.event.MouseEvent[MOUSE_EXITED,(408,14),button=0,clickCount=0] on javax.swing .JButton[,0,345,400x25,...] public abstract void java.awt.event.FocusListener.focusLost(java.awt.event.FocusEvent): java.awt.event.FocusEvent[FOCUS_LOST,temporary,opposite=null] on javax.swing.JButton[,0 ,345,400x25,...]

You may want to capture this output in a file or a console window, as explained in the preceding sections.

Example 11-6 is the Eventtracer class. The idea behind the class is easy even if the implementation is a bit mysterious.

  1. When you add a component to the event tracer in the add method, the JavaBeans introspection class analyzes the component for methods of the form void addXxxListener(XxxEvent). (See Chapter 8 of Volume 2 for more information on JavaBeans.) For each matching method, an EventSetDescriptor is generated. We pass each descriptor to the addListener method.

  2. If the component is a container, we enumerate its components and recursively call add for each of them.

  3. The addListener method is called with two parameters: the component on whose events we want to spy and the event set descriptor. The getListenerType method of the EventSetDescriptor class returns a Class object that describes the event listener interface such as ActionListener or ChangeListener. We create a proxy object for that interface. The proxy handler simply prints the name and event parameter of the invoked event method. The getAddListenerMethod method of the EventSetDescriptor class returns a Method object that we use to add the proxy object as the event listener to the component.

    This program is a good example of the power of the reflection mechanism. We don't have to hardwire the fact that the JButton class has a method addActionListener whereas a JSlider has a method addChangeListener. The reflection mechanism discovers these facts for us.

NOTE

The proxy mechanism makes this program dramatically easier. In prior editions of this book, we needed to define a listener that simultaneously implements the MouseListener, ComponentListener, FocusListener, KeyListener, ContainerListener, WindowListener, TextListener, AdjustmentListener, ActionListener, and ItemListener interfaces and a couple of dozen methods that print the event parameter. The proxy mechanism is explained at the end of Chapter 6.


Example 11-7 tests the event tracer. The program displays a frame with a button and a slider and traces the events that these components generate.

Example 11-6. EventTracer.java
  1. import java.awt.*;  2. import java.awt.event.*;  3. import java.beans.*;  4. import java.lang.reflect.*;  5.  6. public class EventTracer  7. {  8.    public EventTracer()  9.    { 10.       // the handler for all event proxies 11.       handler = new 12.          InvocationHandler() 13.          { 14.             public Object invoke(Object proxy, Method method, Object[] args) 15.             { 16.                System.out.println(method + ":" + args[0]); 17.                return null; 18.             } 19.          }; 20.    } 21. 22.    /** 23.       Adds event tracers for all events to which this component 24.       and its children can listen 25.       @param c a component 26.    */ 27.    public void add(Component c) 28.    { 29.       try 30.       { 31.          // get all events to which this component can listen 32.          BeanInfo info = Introspector.getBeanInfo(c.getClass()); 33. 34.          EventSetDescriptor[] eventSets = info.getEventSetDescriptors(); 35.          for (EventSetDescriptor eventSet : eventSets) 36.             addListener(c, eventSet); 37.       } 38.       catch (IntrospectionException e) {} 39.       // ok not to add listeners if exception is thrown 40. 41.       if (c instanceof Container) 42.       { 43.          // get all children and call add recursively 44.          for (Component comp : ((Container) c).getComponents()) 45.             add(comp); 46.       } 47.    } 48. 49.    /** 50.       Add a listener to the given event set 51.       @param c a component 52.       @param eventSet a descriptor of a listener interface 53.    */ 54.    public void addListener(Component c, EventSetDescriptor eventSet) 55.    { 56.       // make proxy object for this listener type and route all calls to the handler 57.       Object proxy = Proxy.newProxyInstance(null, 58.          new Class[] { eventSet.getListenerType() }, handler); 59. 60.       // add the proxy as a listener to the component 61.       Method addListenerMethod = eventSet.getAddListenerMethod(); 62.       try 63.       { 64.          addListenerMethod.invoke(c, proxy); 65.       } 66.       catch(InvocationTargetException e) {} 67.       catch(IllegalAccessException e) {} 68.       // ok not to add listener if exception is thrown 69.    } 70. 71.    private InvocationHandler handler; 72. } 

Example 11-7. EventTracerTest.java
  1. import java.awt.*;  2. import java.awt.event.*;  3. import javax.swing.*;  4.  5. public class EventTracerTest  6. {  7.    public static void main(String[] args)  8.    {  9.       JFrame frame = new EventTracerFrame(); 10.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 11.       frame.setVisible(true); 12.    } 13. } 14. 15. class EventTracerFrame extends JFrame 16. { 17.    public EventTracerFrame() 18.    { 19.       setTitle("EventTracerTest"); 20.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 21. 22.       // add a slider and a button 23.       add(new JSlider(), BorderLayout.NORTH); 24.       add(new JButton("Test"), BorderLayout.SOUTH); 25. 26.       // trap all events of components inside the frame 27.       EventTracer tracer = new EventTracer(); 28.       tracer.add(this); 29.    } 30. 31.    public static final int DEFAULT_WIDTH = 400; 32.    public static final int DEFAULT_HEIGHT = 400; 33. } 

Letting the AWT Robot Do the Work

Version 1.3 of the Java 2 Platform adds a Robot class that you can use to send keystrokes and mouse clicks to any AWT program. This class is intended for automatic testing of user interfaces.

To get a robot, you need to first get a GraphicsDevice object. You get the default screen device through the sequence of calls:

 GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice screen = environment.getDefaultScreenDevice(); 

Then you construct a robot as:

 Robot robot = new Robot(screen); 

To send a keystroke, tell the robot to simulate a key press and a key release:

 robot.keyPress(KeyEvent.VK_TAB); robot.keyRelease(KeyEvent.VK_TAB); 

For a mouse click, you first need to move the mouse and then press and release a button:

 robot.mouseMove(x, y); // x and y are absolute screen pixel coordinates. robot.mousePress(InputEvent.BUTTON1_MASK); robot.mouseRelease(InputEvent.BUTTON1_MASK); 

The idea is that you simulate key and mouse input and afterwards take a screen snapshot to see whether the application did what it was supposed to. You capture the screen with the createScreenCapture method:

 Rectangle rect = new Rectangle(x, y, width, height); BufferedImage image = robot.createScreenCapture(rect); 

The rectangle coordinates also refer to absolute screen pixels.

Finally, you usually want to add a small delay between robot instructions so that the application can catch up. Use the delay method and give it the number of milliseconds to delay. For example:

 robot.delay(1000); // delay by 1000 milliseconds 

The program in Example 11-8 shows how you can use the robot. A robot tests the button test program that you saw in Chapter 8. First, pressing the space bar activates the leftmost button. Then the robot waits for two seconds so that you can see what it has done. After the delay, the robot simulates the tab key and another space bar press to click on the next button. Finally, we simulate a mouse click on the third button. (You may need to adjust the x and y coordinates of the program to actually press the button.) The program ends by taking a screen capture and displaying it in another frame (see Figure 11-7).

Figure 11-7. Capturing the screen with the AWT robot


As you can see from this example, the Robot class is not by itself suitable for convenient user interface testing. Instead, it is a basic building block that can be a foundational part of a testing tool. A professional testing tool can capture, store, and replay user interaction scenarios and find out the screen locations of the components so that mouse clicks aren't guesswork. At the time of this writing, the robot is brand new and we are not aware of any sophisticated testing tools for Java user interfaces. We expect these tools to materialize in the future.

Example 11-8. RobotTest.java
  1. import java.awt.*;  2. import java.awt.event.*;  3. import java.awt.image.*;  4. import javax.swing.*;  5.  6. public class RobotTest  7. {  8.    public static void main(String[] args)  9.    { 10.       // make frame with a button panel 11. 12.       ButtonFrame frame = new ButtonFrame(); 13.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 14.       frame.setVisible(true); 15. 16.       // attach a robot to the screen device 17. 18.       GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); 19.       GraphicsDevice screen = environment.getDefaultScreenDevice(); 20. 21.       try 22.       { 23.          Robot robot = new Robot(screen); 24.          run(robot); 25.       } 26.       catch (AWTException e) 27.       { 28.          e.printStackTrace(); 29.       } 30.    } 31. 32.    /** 33.       Runs a sample test procedure 34.       @param robot the robot attached to the screen device 35.    */ 36.    public static void run(Robot robot) 37.    { 38.       // simulate a space bar press 39.       robot.keyPress(' '); 40.       robot.keyRelease(' '); 41. 42.       // simulate a tab key followed by a space 43.       robot.delay(2000); 44.       robot.keyPress(KeyEvent.VK_TAB); 45.       robot.keyRelease(KeyEvent.VK_TAB); 46.       robot.keyPress(' '); 47.       robot.keyRelease(' '); 48. 49.       // simulate a mouse click over the rightmost button 50.       robot.delay(2000); 51.       robot.mouseMove(200, 50); 52.       robot.mousePress(InputEvent.BUTTON1_MASK); 53.       robot.mouseRelease(InputEvent.BUTTON1_MASK); 54. 55.       // capture the screen and show the resulting image 56.       robot.delay(2000); 57.       BufferedImage image = robot.createScreenCapture(new Rectangle(0, 0, 400, 300)); 58. 59.       ImageFrame frame = new ImageFrame(image); 60.       frame.setVisible(true); 61.    } 62. } 63. 64. /** 65.    A frame to display a captured image 66. */ 67. class ImageFrame extends JFrame 68. { 69.    /** 70.       @param image the image to display 71.    */ 72.    public ImageFrame(Image image) 73.    { 74.       setTitle("Capture"); 75.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 76. 77.       JLabel label = new JLabel(new ImageIcon(image)); 78.       add(label); 79.    } 80. 81.    public static final int DEFAULT_WIDTH = 450; 82.    public static final int DEFAULT_HEIGHT = 350; 83. } 


 java.awt.GraphicsEnvironment 1.2 

  • static GraphicsEnvironment getLocalGraphicsEnvironment()

    returns the local graphics environment.

  • GraphicsDevice getDefaultScreenDevice()

    returns the default screen device. Note that computers with multiple monitors have one graphics device per screen use the getScreenDevices method to obtain an array of all screen devices.


 java.awt.Robot 1.3 

  • Robot(GraphicsDevice device)

    constructs a robot that can interact with the given device.

  • void keyPress(int key)

  • void keyRelease(int key)

    simulate a key press or release.

    Parameters:

    key

    The key code. See the KeyStroke class for more information on key codes


  • void mouseMove(int x, int y)

    simulates a mouse move.

    Parameters:

    x, y

    The mouse position in absolute pixel coordinates


  • void mousePress(int eventMask)

  • void mouseRelease(int eventMask)

    simulate a mouse button press or release.

    Parameters:

    eventMask

    The event mask describing the mouse buttons. See the InputEvent class for more information on event masks


  • void delay(int milliseconds)

    delays the robot for the given number of milliseconds.

  • BufferedImage createScreenCapture(Rectangle rect)

    captures a portion of the screen.

    Parameters:

    rect

    The rectangle to be captured, in absolute pixel coordinates



       
    top



    Core Java 2 Volume I - Fundamentals
    Core Java(TM) 2, Volume I--Fundamentals (7th Edition) (Core Series) (Core Series)
    ISBN: 0131482025
    EAN: 2147483647
    Year: 2003
    Pages: 132

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net