Sun Java Solaris Communities My SDN Account Join SDN
 
Technical Articles and Tips

JTabbedPane and Using Reflection to Test Methods and Classes

 

Tech Tips Archive


WELCOME to the Java Developer Connection (JDC) Tech Tips, July 12, 2001. This issue covers:

These tips were developed using Java 2 SDK, Standard Edition, v 1.3.

Pixel

JTABBEDPANE

JTabbedPane is a Swing class that you can use when you want several GUI components (such as JPanels) to share the same space. Each component is made visible by selecting a tab.

Here is a simple example of JTabbedPane use:

    import javax.swing.*;
    import java.awt.event.*;
    
    public class JTabDemo1 {
        public static void main(String args[]) {
            JFrame frame = new JFrame("JTabDemo1");
    
            // handle window close
    
            frame.addWindowListener(new 
                      WindowAdapter() {
                public void windowClosing(WindowEvent 
                    e) {
                    System.exit(0);
                }
            });
    
            // set up panels with buttons
    
            JPanel panel1 = new JPanel();
            JPanel panel2 = new JPanel();
    
            panel1.add(new JButton("Button in panel 1 
		                         in tab 1")); 
            panel2.add(new JButton("Button in panel 2 
	  				 in tab 2"));
    
            // set up JTabbedPane object and 
            // add panels
    
            JTabbedPane jtp = new JTabbedPane();
    
            jtp.add("Tab 1", panel1);
            jtp.add("Tab 2", panel2);
    
            // display
    
            frame.getContentPane().add(jtp);
            frame.setLocation(200, 200);
            frame.pack();
            frame.setVisible(true);
        }
    }

The JTabDemo1 program sets up two panels, each with a button. The program adds the panels to a JTabbedPane object, and gives each panel a title such as "Tab 1." When you run the program, you can click on the tabs to switch between panels. There is one tab-component pair selected at any given time. Tabs are referenced by an index between 0 and the number of tabs minus 1.

Here's another example of using JTabbedPane:

    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.event.*;
    
    public class JTabDemo2 {
        public static void main(String args[]) {
            JFrame frame = new JFrame("JTabDemo2");
    
            // handle window close
    
            frame.addWindowListener(new 
                      WindowAdapter() {
                public void windowClosing(WindowEvent 
                                                 e) {
                    System.exit(0);
                }
            });
    
            // set up panels with buttons
    
            JPanel panel1 = new JPanel();
            JPanel panel2 = new JPanel();
    
            panel1.add(new JButton("Button in panel 1 
                                         in tab 1"));
            panel2.add(new JButton("Button in panel 2 
					 in tab 2"));
    
            // set up JTabbedPane object
    
            final JTabbedPane jtp =
                new JTabbedPane(SwingConstants.BOTTOM);
    
            // set up listener for JTabbedPane object
    
            jtp.addChangeListener(new ChangeListener() {
                public void stateChanged(ChangeEvent e) {
                    int index = jtp.getSelectedIndex();
                    String title = jtp.getTitleAt(index);
                    System.out.println("index = " + 
                                            index);
                    System.out.println("title = " + 
 					    title);
                }
            });
    
            // add tabs, including tooltips and colors 
            // and panels
    
            jtp.add("Tab 1", panel1);
            jtp.addTab("Tab 2", null, panel2, "Tab 2 
                                              Tip");
            jtp.setForegroundAt(0, Color.blue);
            jtp.setBackgroundAt(1, Color.red);
    
            // display
    
            frame.getContentPane().add(jtp);
            frame.setLocation(200, 200);
            frame.pack();
            frame.setVisible(true);
        }
    }

The JTabDemo2 program shows some of the additional features you can use with JTabbedPane. In this program, the tabs are displayed at the bottom of the pane instead of the top; a tooltip is added for the second tab (you see the tooltip when you pass the mouse pointer over the second tab); and background and foreground colors are set for the tabs. The program also sets up a listener so that tab selection events are captured. For example, if you select the first tab, you see the following displayed in your console:

    index = 0
    title = Tab 1

You can use keyboard keys to navigate JTabbedPane objects. The left and right arrow keys are used to move between tabs, and the Tab key is used to move from a tab to the underlying component. You can also specify icons for display with the title on each tab.

JTabbedPane is similar to CardLayout, which allows you to display one component from a "deck" of components.

For more information about JTabbedPane, see the JTabbedPane section in Chapter 12, Lightweight Containers, in "Graphic Java: Mastering the JFC, 3rd Edition Volume II Swing" by David Geary.

USING REFLECTION TO TEST METHODS AND CLASSES

You can use the Java reflection package to find out about Java types from inside a running program. For example, you can get a list of all the method names for a particular class, and display that list. Or you can obtain a java.lang.reflect.Method object that represents a particular method, and use that object as a sort of pointer to the method.

Suppose you're learning about reflection. You have an application in which you need to call a specific method on an object. So you code the application like this:

    import java.lang.reflect.*;
    
    class A {
        public void f(int i) {
            System.out.println("A.f called  i = " + i);
        }
    }
    
    public class RefDemo1 {
        public static void main(String args[]) throws 
                                          Exception {
            A aref = new A();
    
            // find a method in "A" named "f"
            // and with a single "int" parameter
    
            Method meth = A.class.getMethod("f",
                new Class[]{int.class});
    
            // invoke the method on the "aref" object
    
            meth.invoke(aref, new Object[]{new 
				Integer(37)});
        }
    }

The RefDemo1 program creates an A object, and then calls method f on the object, using reflection.

This approach certainly works, but it represents a convoluted way to make a simple method call. Instead of using reflection in this example, it would be much simpler to say:

    aref.f(37);

to call the method.

If this example isn't a good place to use reflection, then what is? This tip tries to answer this question, by presenting a program example of what might be called an "interpreter" or an "object exerciser." The program reads lines of input from a file or the keyboard. Then based on the data in these lines, the program creates objects and calls methods on them.

Here's an example of interpreter input:

    > new java.lang.String ABC

    > call toLowerCase abc

The first line tells the interpreter to create a new String object, with the string ABC as the argument to the constructor. The second line tells the interpreter to call the method toLowerCase. The second line also tells the interpreter that the method must return abc or else an error message is produced.

Each method has arguments and a return value specified. After the method is called, the return value specified in the input file is checked against the actual return value. An error is flagged if the values are not equal.

Using this approach, it's possible to test methods by simply writing scripts that list method names, arguments to the methods, and the expected return value.

How would you implement such a program? It's clear that the user can specify arbitrary classes and methods, whose names exist in string form within the program. These strings somehow need to be mapped into actual classes and methods. That is, if the program has a string with the name of a class, it needs to actually create an instance of this class. The program then needs to find and call methods of this class, given strings specifying method names.

Reflection is specifically designed for this kind of programming area. Using reflection, it's possible to create class instances from string names, and then look up and execute methods by name. This dynamic feature of a programming language is sometimes termed "late binding." By contrast, languages such as C and C++ use "early binding," that is class/function names are not kept around at run time.

Here is the interpreter program:

    import java.io.*;
    import java.util.*;
    import java.lang.reflect.*;
    
    class Interpreter {
        // input line and list of tokens from line
        private String input;
        private List tokenlist;
    
        // current class and object of that class
        private Class currcls;
        private Object currobj;
    
        // execute a line of input
        public void execLine(String line) {
    
            // tokenize the input line and return 
	    // if line is blank
    
            input = line;
            getTokens();
            if (tokenlist.size() == 0) {
                return;
            }
    
            System.out.println("Executing line: " + 
					    input);
    
            // get type of line (new or call) 
	    // and dispatch
    
            String type = (String)tokenlist.get(0);
            if (type.equals("new")) {
                execNew();
            }
            else if (type.equals("call")) {
                execCall();
            }
            else {
                msg("Invalid operator on line");
                return;
            }
        }
    
        // create a new object of a class and
        // make it the current object
        private void execNew() {
            if (tokenlist.size() < 2) {
                msg("Missing class name");
                return;
            }
    
            // load the class if not already done
    
            try {
                currcls = class.forname((string)tokenlist.get(1));
            }
            catch (classnotfoundexception e) {
                msg("ClassNotFoundException in forName");
                return;
            }
    
            // get arguments to constructor
    
            currobj = null;
            class args[] = getargtypes(2);
            object vals[] = getargvalues(2);
            constructor ctor;
    
            // find the constructor
    
            try {
                ctor = currcls.getconstructor(args);
            }
            catch (nosuchmethodexception e) {
                msg("NoSuchMethodException in 
		getConstructor");
                return;
            }
    
            // create a new instance of the object
    
            try {
                currobj = ctor.newinstance(vals);
            }
            catch (instantiationexception e) {
                msg("InstantiationException in 
				newInstance");
                return;
            }
            catch (illegalaccessexception e) {
                msg("IllegalAccessException in 
				newInstance");
                return;
            }
            catch (invocationtargetexception e) {
                msg("InvocationTargetException in 
 				   newInstance");
                return;
            }
        }
    
        // call a method for the current object
        private void execcall() {
            if (tokenlist.size() < 3) {
                msg("Missing method or return value");
                return;
            }
            if (currobj == null) {
                msg("No current class object");
                return;
            }
    
            // get the method name and the arguments 
	    // to the method
    
            string methname = (string)tokenlist.get(1);
            object ret = getret();
            class args[] = getargtypes(3);
            object vals[] = getargvalues(3);
            method meth;
            object retobj;
    
            // find the method in the class
    
            try {
                meth = currcls.getmethod(methname, args);
            }
            catch (nosuchmethodexception e) {
                msg("Method not found");
                return;
            }
    
            // invoke the method
    
            try {
                retobj = meth.invoke(currobj, vals);
            }
            catch (illegalaccessexception e) {
                msg("IllegalAccessException in invoke");
                return;
            }
            catch (invocationtargetexception e) {
                msg("InvocationTargetException in 
					invoke");
                return;
            }
    
            // check return value if any
    
            if (ret != null && !ret.equals(retobj)) {
                msg("Invalid return value from method");
                return;
            }
        }
    
        // tokenize the input line
        private void gettokens() {
            tokenlist = new arraylist();
            int strlen = input.length();
            int i = 0;
    
            for (;;) {
                while (i < strlen &&
                    character.iswhitespace(
                        input.charat(i))) {
                    i++;
                }
                if (i == strlen) {
                    break;
                }
                stringbuffer sb = new stringbuffer();
                while (i < strlen &&
                    !character.iswhitespace(
                                input.charat(i))) {
                    sb.append(input.charat(i));
                    i++;
                }
                tokenlist.add(sb.tostring());
            }
        }
    
        // get the return value for a method call
        private object getret() {
            string s = (string)tokenlist.get(2);
            if (s.equals("void")) {
                return null;
            }
            else if (isnum(s)) {
                return new integer(s);
            }
            else {
                return s;
            }
        }
    
        // get types of the arguments to a 
	// constructor or method
        private class[] getargtypes(int start) {
            int numargs = tokenlist.size() - start;
            class args[] = new class[numargs];
            int j = 0;
            for (int i = start; i < tokenlist.size(); 
					       i++) {
                string s = (string)tokenlist.get(i);
                args[j++] = isnum(s) ? int.class : 
                                     string.class;
            }
            return args;
        }
    
        // get the argument values for a constructor 
	// or method call
        private object[] getargvalues(int start) {
            int numargs = tokenlist.size() - start;
            object args[] = new object[numargs];
            int j = 0;
            for (int i = start; i < tokenlist.size(); 
                                               i++) {
                string s = (string)tokenlist.get(i);
                args[j++] = isnum(s) ? (object)new 
                                      integer(s) :
                    (object)s;
            }
            return args;
        }
    
        // display an error message
        private static void msg(string txt) {
            system.out.println("*** " + txt + " ***");
        }
    
        // determine whether a string is a number 
	// nnn or -nnn
        private static boolean isnum(string s) {
            int slen = s.length();
            int i = slen >= 2 && s.charAt(0) == '-' ? 
					       1 : 0;
            for (; i < slen; i++) {
                if (!character.isdigit(s.charat(i))) {
                    return false;
                }
            }
            return true;
        }
    }
    
    public class refdemo2 {
        public static void main(string args[]) throws 
 					ioexception {
            reader r;
            boolean isterm = false;
    
            // use command-line file if present, 
	    // else standard input
    
            if (args.length == 1) {
                r = new filereader(args[0]);
            }
            else {
                r = new inputstreamreader(system.in);
                isterm = true;
            }
            bufferedreader br = new bufferedreader(r);
    
            interpreter in = new interpreter();
    
            // read input lines and dispatch
    
            for (;;) {
                if (isterm) {
                    system.out.print("> ");
                }
                String inputline = br.readLine();
                if (inputline == null) {
                    break;
                }
                in.execLine(inputline);
            }
    
            br.close();
        }
    }


The program reads lines from a file if a file name is specified on the command line. Otherwise, the program prompts for input (with a ">" prompt). The program breaks each input line into tokens; tokens are entries in the line separated by white space. The tokens are then stored in a list (tokenlist) as strings. Lines without any tokens are ignored.

Input lines of this form:

    new classname arg1 arg2 ...

create a new class instance, which becomes the "current object." You can specify arguments to the constructor. In this case, the reflection mechanism is used to locate and then invoke the appropriate constructor. Arguments of the form NNN or -NNN are considered integers; all other arguments are treated as strings. This approach is sufficient to illustrate the concept, although it would be desirable to handle other argument types such as floating-point.

Lines of this form:

    call methodname returnvalue arg1 arg2 ...

invoke a method on the current object, using the method name and types of the arguments (int or String) to locate and invoke an appropriate method in the class represented by the current object.

Here's an example input script that might be used to test java.lang.String:

    new java.lang.String ABC
    call toLowerCase abc

    new java.lang.String abc
    call concat abcdef def

    new java.lang.String abc
    call indexOf 2 c

Store the script in a file named script. Then run the RefDemo2 interpreter program like this:

    java RefDemo2 script

You should see the following display:

    Executing line:     new java.lang.String ABC
    Executing line:     call toLowerCase abc
    Executing line:     new java.lang.String abc
    Executing line:     call concat abcdef def
    Executing line:     new java.lang.String abc
    Executing line:     call indexOf 2 c 

If you change the "2" to "3" on the last line, the interpreter program will flag an error as follows, indicating there's a bug either in the implementation of the method (indexOf) or in the test case:

    Executing line:     call indexOf 3 c
    *** Invalid return value from method ***

Here's another example of using an interpreter program. Suppose you have a class that looks like this:

    public class RefTest {
        public int sum(int a, int b) {
            return a + b;
        }
        public static int stsum(int a, int b) {
            return a + b;
        }
    }

You could write some tests for it using this script:

    new RefTest
    
    call sum 10 4 6
    call sum 0 -5 5
    call sum 0 0 0
    
    call stsum 10 4 6
    call stsum 0 -5 5
    call stsum 0 0 0

and run them by saying:

    javac RefTest.java

    javac RefDemo2.java

    java RefDemo2 script

You should see:

    Executing line:     new RefTest
    Executing line:     call sum 10 4 6
    Executing line:     call sum 0 -5 5
    Executing line:     call sum 0 0 0
    Executing line:     call stsum 10 4 6
    Executing line:     call stsum 0 -5 5
    Executing line:     call stsum 0 0 0

The interpreter program illustrates how reflection is useful in a particular family of applications. These kind of applications accept arbitrary user input and use it to manipulate objects by class and method name within a program. For presentation purposes, this tip simplified some of the details and glossed over some issues. But the program does illustrate the power of reflection to solve a particular type of problem.

For more information about reflection, see Section 11.2, Reflection, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes

Pixel

CORRECTION

In the June 12, 2001 issue of the JDC Tech Tips, there is a paragraph in the tip "Using Peer Classes With the Java Native Interface" that reads:

The destroy method is synchronized to avoid race conditions. The create method is called from the constructor, which can execute in only one thread for a given object. Also the getvalue method is read-only, so there are no issues with synchronization for these two methods.

The last sentence of this paragraph is wrong. The getValue method is indeed read-only, but it does need to be synchronized. The reason is simply that getValue may be executing in one thread at the same time that another thread is executing the destroy method. If so, a value of 0 for "peerobj" can be passed to the underlying native method for getValue, possibly resulting in a segmentation violation (crash). This issue is further discussed in section 10.3.1, synchronized Methods, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes

Also, in the example presented in that tip, both the Java method destroy() and the native method destroy(long) are synchronized. The synchronized declaration of the native method is redundant, given that it's called from a wrapper method that is itself synchronized.

- NOTE

Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun Microsystems purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page, uncheck the appropriate checkbox, and click the Update button.

- SUBSCRIBE

To subscribe to a JDC newsletter mailing list, go to the Subscriptions page, choose the newsletters you want to subscribe to, and click Update.

- FEEDBACK

Comments? Send your feedback on the JDC Tech Tips to:

jdc-webmaster@sun.com

- ARCHIVES

You'll find the JDC Tech Tips archives at: http://java.sun.com/jdc/TechTips/index.html

- COPYRIGHT

Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:

http://java.sun.com/jdc/copyright.html

- LINKS TO NON-SUN SITES

The JDC Tech Tips may provide, or third parties may provide, links to other Internet sites or resources. Because Sun has no control over such sites and resources, You acknowledge and agree that Sun is not responsible for the availability of such external sites or resources, and does not endorse and is not responsible or liable for any Content, advertising, products, or other materials on or available from such sites or resources. Sun will not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such Content, goods or services available on or through any such site or resource.

This issue of the JDC Tech Tips is written by Glen McCluskey.

JDC Tech Tips July 12, 2001

Sun, Sun Microsystems, Java, and Java Developer Connection are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.