Richard G Baldwin (512) 223-4758, baldwin@austin.cc.tx.us, http://www2.austin.cc.tx.us/baldwin/

Reflection and the Method Class - III

Java Programming, Lecture Notes # 262, Revised 02/13/98.

Preface

Students in Prof. Baldwin's Advanced Java Programming classes at ACC are responsible for knowing and understanding all of the material in this lesson.

Introduction

In an earlier lesson, I promised you that I was going to give you a practical example of the use of the invoke() method of the Method class of the reflection API. Well, this is it.

Note that this lesson also makes heavy use of Hashtables. If you aren't really comfortable with the use of a hashtable, you might want to go back and review the lesson on Vectors, Enumerations, and Hashtables. As of the time of this writing, that was lesson 76 in the Intermediate Java tutorial.

In this lesson, we will develop a sample program that is a sample of a large class of programs often referred to as smart adapters. The form of the program that we will develop will only be indicative of what can be accomplished using numerous variations of this powerful technique.

Sample Program

This program uses reflection and hashtables to receive ActionEvents from several different sources and route them to the same or different destination methods on the same or different destination objects.

In other words, an action event handler is designed to operate in between the source of the event and the method that will ultimately respond to the event. In one case, this event handler provides some filtering capability. There many variations on the benefits that can accrue from an operation structured in this manner.

There can be a very large number of sources, and they can be any objects capable of generating action events.

By making a fairly simple modification to the program to give it the capability to unregister previously registered associations, the manner in which events are delivered to destination methods could be modified at runtime, and could change during the execution of a program.

A primary requirement is that the formal argument list for all destination methods must be the same.

Typically the formal argument list would include an object of type ActionEvent. In this program, it also includes an object of type Date() in order to timestamp the events.

A reader has pointed out that for clarification, I should remind everyone that even though I am passing an object of type ActionEvent to the destination method, it is not a "standard" action listener method. Rather, it is one step removed from the action listener method (named actionPerformed) and is being called by the action listener method. That is why I can get by with passing a Date object in addition to the ActionEvent object.

The same destination method can receive events from two or more sources.

The same source can be registered on only one destination. In this program, once a source is registered on a given destination, any attempt to register that source on another destination will result in the attempt being ignored and an exception being thrown. However, nothing is done with the exception in this program other than to display it for demonstration purposes.

Provisions were not made to unregister a source from a destination, but it wouldn't be difficult to do so. This would make it possible to rearrange the source-destination links at runtime.

In addition to providing the routing capability described above, this program also demonstrates an event filtering capability.

Event filtering is usually interpreted to mean that the event handler does some preprocessing of event data before delivering the event to the destination method. In some cases, the data could be modified before forwarding the event, and in some cases, the event might not be forwarded at all. The latter is the case in this demonstration program.

If the source of the event is a TextField and the text value is an empty String, the event is ignored and is not delivered to the registered destination method.

Now lets discuss some of the details of operation.

An ActionListener class is defined that receives action events from any number of sources and dispatches them to the same or different methods on the same or different destination objects. It uses a Hashtable as the dispatching table to associate the event source with the required destination object and the required method in that object.

The general methodology is to save an object containing a reference to a Method object that represents the destination method and a reference to the destination object along with the associated action event source in a hash table. The event source is the key. The object containing the two references is the value in the key/value pair of a hashtable.

When an action event occurs later, the program uses the event source as the key to retrieve the object containing the references to the Method object and the destination object from the table.

The program then uses the the invoke() method of the Method class on the Method object to invoke the destination method on the destination object.

To do all of this, it is first necessary to create the Method object that represents a destination method. This requires the name of the destination method along with an array of type Class whose elements represent the types of the arguments in the formal argument list of the destination method.

Since it is assumed that the types of the formal argument list are known and may not change in this demonstration program, a static final array of type Class is instantiated and initialized with this information. This array is named destinationMethodArgTypes.

A new instance of a Hashtable is also declared and instantiated when the class is loaded. This object will be used as the dispatching table.

The class contains a method named registerDestinationMethod() that receives three parameters.

This method uses the name of the destination method, along with the Class array named destinationMethodArgTypes, as parameters to the getMethod() method of the Class class to create an object of type Method that represents the destination method.

A reference to this Method object, along with a reference to the destination object are encapsulated in an object of type Combine.

Then an entry is made in the dispatch hash table with the action event source as the key and the reference to the object of type Combine as the value. This makes it possible to come back later and retrieve the reference to the Method object and the reference to the destination object from the dispatch table using the source of an action event as the key.

All classes that service action events must define the actionPerformed() method because it is declared in the ActionListener interface. Therefore, this class has an overridden actionPerformed() method.

When an action event is generated by a source object on which this listener object is registered, the actionPerformed() method of this object is invoked. This method uses the source of the event as the key and extracts the object of type Combine from the Hashtable. It then extracts the reference to the Method object and the reference to the destination object from that object.

This Method object, along with the invoke() method of the Method class will be used to invoke the destination method on the destination object that is associated with the source of the event in the dispatch table.

This invoke() method requires two parameters.

In this program, the destination method is defined to require two parameters. The requisite Object array is instantiated and populated.

Then the invoke() method is invoked on the reference to the object of type Method that was extracted from the dispatch Hashtable with the following being passed as parameters.

This causes the destination method to be invoked on the destination object.

Beyond this, everything is pretty straightforward. Two classes are defined from which destination objects can be instantiated.

One of these classes defines two different destination methods that have the same required formal argument list. The other class defines one destination method with the same formal argument list.

To keep things as simple as possible, all that these methods do is to display some information when they are invoked, but they could be put to useful purposes in a real program. If they are modified to do anything that is expected to consume a lot of time, they should spawn another thread to do the work and return as soon as possible.

A class named GUI is defined that instantiates and adds three Button objects and a TextField object to a Frame object. These components are used as sources of action events for test purposes.

Then the program instantiates two destination objects and an action listener object. It registers the destination methods and destination objects on the sources of action events by way of the action listener object, specifying which source is to be associated with which destination method and destination object. Several combinations of associations are made between event sources and destination methods.

Then the action listener object is registered on all four event sources where it will receive all action events generated by those sources and dispatch them to the requisite destination method.

If the action listener object receives an action event from the TextField object at a time when the TextField is empty, the event will be filtered out and will not be dispatched to its intended destination.

Finally, a WindowListener object is registered on the Frame so that the program will terminate when the user closes the Frame object.

This program was tested using JDK 1.1.3 under Win95.

The output produced by the program when action events were generated on all four components in left to right order will be shown later.

Interesting Code Fragments

With that explanation of the program behind us, let's look at some code. We will begin with the ActionListener class, and will break it up and discuss it in pieces.

This is the ActionListener class that receives action events and dispatches them to the same or different methods on the same or different destination objects. It uses a Hashtable as the dispatching table to associate the event source with the required destination object and method.

The first interesting code fragment declares, instantiates, and initializes a static final array of type Class which contains objects that represent the types of arguments in the formal argument list. (Note the braces used in the initialization of this array.)

It also instantiates a new Hashtable object.
 
  final static Class[] destinationMethodArgTypes = 
                          {ActionEvent.class, Date.class};
  Hashtable dispatchTable = new Hashtable();
Two different pieces of information are going to be associated with each key in the Hashtable object. Therefore, we will define an inner class that can be used to encapsulate the two pieces of information into a single object that can be referenced as a value in the Hashtable object.

The definition of that inner class is shown below. Note that the instance variables in this class have default or package access, making them directly available to other code later in the program.
 
  class Combine{
    Object theDestinationObj;
    Method theMethod;
    //---------------------------------------------------//
    
    //Constructor for inner class
    Combine(Object theDestinationObj, Method theMethod){
      this.theDestinationObj = theDestinationObj;
      this.theMethod = theMethod;
    }//end constructor for inner class
  }//end class Combine  
This is followed by the method named registerDestinationMethod() that is used to register destination methods on event sources. This is what instructs the action listener object as to which destination method on which destination object is to be notified when an action event occurs on a particular source. Recall that a source can be registered on only one destination method.

This is a fairly long method, so we will probably need to break it into smaller fragments and discuss them separately.

The first fragment simply shows the method header and shows how the Class object representing the destination object is obtained by invoking the getClass() method on the reference to the destination object. This Class object is required in order to get the Method object that represents the method of interest.
 
  public void registerDestinationMethod(
                 Object destinationObj,Object whichSource,
                                        String methodName){

    Class theDestinationObjClass = 
                                 destinationObj.getClass();
I'm going to ignore the try/catch blocks in reviewing this code.

The next code fragment uses the getMethod() method of the Class class to create a Method object that represents a method with a matching name and formal argument list. Note that this method needs the name of the method as a String object and the Class array that represents the types of the arguments of the method as parameters.
 
      Method theMethod = theDestinationObjClass.getMethod(
                     methodName,destinationMethodArgTypes);
Once we have the Method object, we can encapsulate it, along with the reference to the destination object, in an object of type Combine for storage in the Hashtable.
 
    Combine combinedStuff = 
                     new Combine(destinationObj,theMethod);
Now it is time to make an entry in the Hashtable. This entry associates a reference to an object that is the source of an action event with a reference to an object of type Combine that contains a reference to a Method object representing the method that is to be invoked whenever that source generates an event and also contains a reference to the object that contains the method.

We don't want to allow duplicate keys (duplicate references to the same event source) so we will test for duplicate keys at this point. If a duplicate key is specified, we will ignore it (not put it in the table) and throw an exception.
 
      if(!dispatchTable.containsKey(whichSource))
        dispatchTable.put(whichSource,combinedStuff);
      else throw new IllegalAccessException();
Following this is the actionPerformed() method which is invoked whenever a source that this listener object is registered on generates an action event.

This method goes into the dispatch table with a reference to the the source object of the event and comes back with a reference to an object that contains a reference to the Method object that is associated with that source and a reference to the destination object that contains the method.

Then it uses the Method object to invoke the destination method on the destination object, passing a reference to the ActionEvent object and a reference to a Date() object as parameters to the method.

However if the source is a TextField object which contains an empty string, the program filters the event and doesn't forward it to its intended receiver.

We will also break this method into a couple of fragments and review them separately. As before, we will ignore the try/catch blocks.

When this method is invoked, it receives an ActionEvent object from the source object. The reference to this object is known locally as evt.

We invoke the getSource() method on the ActionEvent object to obtain a reference to the object that generated the event. This becomes the key by which we will access our Hashtable.

There is some pretty ugly downcasting going on here, because only references of type Object are stored in a Hashtable. Whenever we access a Hashtable, we normally need to downcast the result to make it useful.

Once we have a reference to the object of type Combine named combinedStuff, we can access the two instance variables of that object directly (as mentioned earlier). This gives us references to the two objects that we need for the next step in the process.
 
      Object key = evt.getSource();
      Combine combinedStuff = 
                          (Combine)dispatchTable.get(key);
      Method theMethod = (Method)combinedStuff.theMethod;
      Object theDestinationObj = 
                  (Object)combinedStuff.theDestinationObj;
At this point, we are going to do some filtering just to illustrate how it might be done. We test to determine if the source object for the event was a TextField object, and if so we test to determine it it contained an empty string when the event occurred. If it was not a TextField object, or if it was a TextField object but did not contain an empty string, we go ahead with the dispatching operation.

If it was a TextField object containing an empty string, we simply bypass the dispatching operation.

The dispatching operation consists of two steps:

Instantiate an array of type Object containing the actual parameters to be passed to the method.

Use the invoke() method of the Method class to invoke the method represented by the Method object on the destination object, passing the array of parameter values as a parameter to the invoke() method. If this doesn't make sense, go back and review the simple program on the invoke() method in an earlier lesson.
 
      if( !((key instanceof TextField) 
           && (((TextField)key).getText().equals("") )) ){
        Object methodParameters[] = { evt, new Date() };
        theMethod.invoke(
                      theDestinationObj,methodParameters);
      }//end if
And that is the end of the actionPerformed() method and also the end of the ActionListener class.

This is followed by a couple of simple class definitions that are used in the demonstration program to instantiate destination objects containing destination methods. There is nothing in these classes that you haven't already seen many times before, so I am not going to discuss them.

This is then followed by the class named GUI that is used to exercise the ActionListener class. Much of this is completely standard stuff, so I am going to delete lots of code and replace that code by comments. I will keep the statements that are interesting insofar as this smart adapter program is concerned.

As you review this code, recall that the argument list for the method named registerDestinationMethod() consists of a reference to the destination object, a reference to a specific source object for action events, and a String object containing the name of the method to be invoked. I have extracted one such statement and presented it below for your review.
 
   actionListener.registerDestinationMethod(
                destinationObject0,myButton0,"abcMethod0")
This is the only thing that is really new in this entire test program.
 
class GUI{
  GUI(){//constructor
    //Put three buttons and a TextField object in a Frame
    // and make it visible

    //Instantiate the destination objects that contain the
    // destination methods
    abcClass destinationObject0 = new abcClass();
    defClass destinationObject1 = new defClass();

    //Instantiate the action listener object
    MyActionListener actionListener = 
                                   new MyActionListener();

    //MAKE CERTAIN THAT YOU UNDERSTAND THE DIFFERENCE
    // BETWEEN THE FOLLOWING TWO REGISTRATION PROCESSES.

    //The following code registers destination methods on
    // specific sources by informing the actionListener
    // object which method on which destination object
    // is to be invoked whenever one of the sources
    // generates an action event.

    actionListener.registerDestinationMethod(
                destinationObject0,myButton0,"abcMethod0");
    actionListener.registerDestinationMethod(
              destinationObject0,myTextField,"abcMethod0");
    actionListener.registerDestinationMethod(
                destinationObject0,myButton1,"abcMethod1");
    actionListener.registerDestinationMethod(
                 destinationObject1,myButton2,"defMethod");
                 
    //Force an exception to be thrown by attempting to 
    // register the same source on two different 
    // destinations.
    actionListener.registerDestinationMethod(
                 destinationObject1,myButton0,"defMethod");
    
    //The following code registers the actionListener
    // object on the individual sources
    myButton0.addActionListener(actionListener);
    myTextField.addActionListener(actionListener);
    myButton1.addActionListener(actionListener);
    myButton2.addActionListener(actionListener);

    //snip
  }//end constructor
}// end class GUI
The output from running this program and generating an action event on each of the objects in the Frame going from left to right is shown below. Manual line breaks were inserted to force this material to fit on the page.

You should be able to explain this output on the basis of what you have seen above.
 
java.lang.IllegalAccessException
Attempt to register same source 
on more than one method
In object :abcClass@1ccc1b
In abcMethod0 java.awt.event.ActionEvent[
  ACTION_PERFORMED,cmd=Button0] on button0
Time Stamp Wed Jan 07 18:23:49 CST 1998

In object :abcClass@1ccc1b
In abcMethod0 java.awt.event.ActionEvent[
  ACTION_PERFORMED,cmd=TextField] on textfield0
Time Stamp Wed Jan 07 18:23:53 CST 1998

In object :abcClass@1ccc1b
In abcMethod1 java.awt.event.ActionEvent[
  ACTION_PERFORMED,cmd=Button1] on button1
Time Stamp Wed Jan 07 18:23:55 CST 1998

In object :defClass@1ccc1d
In defMethod java.awt.event.ActionEvent[
  ACTION_PERFORMED,cmd=Button2] on button2
Time Stamp Wed Jan 07 18:23:56 CST 1998
A complete listing of the program follows in the next section.

Program Listing

/*File Reflections06.java Copyright 1998, R.G.Baldwin

This program uses reflection and hash tables to receive
action events from several different sources and route 
them to the same or different destination methods on 
the same or different
destination objects.

This program was tested using JDK 1.1.3 under Win95.

**********************************************************/
import java.lang.reflect.*;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
//=======================================================//

class Reflections06 {
  public static void main(String[] args) {
    //Instantiate a GUI object to control the program.
    GUI guiObj = new GUI();
  }//end main()
}//end Reflections06
//=======================================================//

//This is an ActionListener class that receives 
// action events and dispatches them to the same or
// different methods on the same or different destination
// objects. It uses a Hashtable as the dispatching table
// to associate the event source with the required 
// destination object and method.
class MyActionListener implements ActionListener{
  
  //Initialize the array of Class objects to match the
  // required types in the formal argument list of the
  // destination methods.
  final static Class[] destinationMethodArgTypes = 
                          {ActionEvent.class, Date.class};
  Hashtable dispatchTable = new Hashtable();
  //-----------------------------------------------------//
  
  //This inner class is used to encapsulate the reference
  // to the Method object and the reference to the
  // destination object in a new object that can be
  // referenced by the value component of a hashtable.
  class Combine{
    Object theDestinationObj;
    Method theMethod;
    //---------------------------------------------------//
    
    //Constructor for inner class
    Combine(Object theDestinationObj, Method theMethod){
      this.theDestinationObj = theDestinationObj;
      this.theMethod = theMethod;
    }//end constructor for inner class
  }//end class Combine  
  //-----------------------------------------------------//
  
  //This method is used to register specific destination
  // methods and their objects with specific action event
  // sources.
  public void registerDestinationMethod(
                 Object destinationObj,Object whichSource,
                                        String methodName){

    Class theDestinationObjClass = 
                                 destinationObj.getClass();
    
    try{
      //Use the getMethod() method of the Class class to
      // create a Method object that represents a method
      // with a matching name and formal argument list.
      Method theMethod = theDestinationObjClass.getMethod(
                     methodName,destinationMethodArgTypes);

    //Encapsulate the two refrences in a new object for
    // referencing in the dispatch table.
    Combine combinedStuff = 
                     new Combine(destinationObj,theMethod);

                     
      //Make an entry in the dispatch table that associates
      // the source of an action event with a reference
      // to an object that contains a reference to method
      // that is to be invoked whenever that source
      // generates an event and a reference to the object
      // that contains the method. 
      
      //Don't allow duplicate keys (duplicate references
      // to the same event source).  If a duplicate is
      // specified, ignore it and throw an exception.
      if(!dispatchTable.containsKey(whichSource))
        dispatchTable.put(whichSource,combinedStuff);
      else throw new IllegalAccessException();
    }//end try block
    catch(NoSuchMethodException e){System.out.println(e);}
    catch(IllegalAccessException e){
      System.out.println(e);
      System.out.println("Attempt to register same " +
                       "source \non more than one method");
      }//end catch block
    
  }//end registerDestinationMethod()
  //-----------------------------------------------------//
  
  //This actionPerformed() method is invoked whenever a
  // source that this listener object is registered on 
  // generates an action event.  This method goes into the
  // dispatch table with the source of the event and
  // comes back with a reference to an object that 
  // contains a reference to the Method object that is 
  // associated with that source and a reference to the
  // destination object that contains the method.  Then it
  // uses the Method object to invoke the destination 
  // method on the destination object, passing the 
  // ActionEvent object along with a Date() object.
  
  //If the source is a TextField object which returns
  // an empty string, filter the event and don't forward
  // it to its intended receiver.
  public void actionPerformed(ActionEvent evt){
    try{
      Object key = evt.getSource();
      Combine combinedStuff = 
                          (Combine)dispatchTable.get(key);
      Method theMethod = (Method)combinedStuff.theMethod;
      Object theDestinationObj = 
                  (Object)combinedStuff.theDestinationObj;
      
      //Filter out empty strings from TextField object
      if( !((key instanceof TextField) 
           && (((TextField)key).getText().equals("") )) ){
        //Instantiate and populate an array of Object 
        // references with the parameters that are to be
        // passed to the destination method when it is 
        // invoked.  Note the initialization syntax.
        Object methodParameters[] = { evt, new Date() };
      
        //Invoke the destination method on the destination
        // object passing the parameters generated above.
        theMethod.invoke(
                      theDestinationObj,methodParameters);
      }//end if
    }//end try
    catch(InvocationTargetException e){
                                    System.out.println(e);}
    catch(IllegalAccessException e){System.out.println(e);}

  }//end actionPerformed()
}//end class MyActionListener

//=======================================================//
//Objects of this class can be registered on the 
// ActionListener object to have specific methods
// invoked when specific sources generate action events.
class abcClass{
  public void abcMethod0(ActionEvent e, Date d){
    System.out.println("In object :" + this);
    System.out.println("In abcMethod0 " + e);
    System.out.println("Time Stamp " + d + "\n");
  }//end abcMethod0
  //-----------------------------------------------------//
  
  public void abcMethod1(ActionEvent e, Date d){
    System.out.println("In object :" + this);
    System.out.println("In abcMethod1 " + e);
    System.out.println("Time Stamp " + d + "\n");
  }//end abcMethod1
}//end abcClass
//=======================================================//

//Objects of this class can also be registered on the 
// ActionListener object to have specific methods
// invoked when specific sources generate action events.
class defClass{
  public void defMethod(ActionEvent e, Date d){
    System.out.println("In object :" + this);
    System.out.println("In defMethod " + e);
    System.out.println("Time Stamp " + d + "\n");
  }//end defMethod
}//end defClass
//=======================================================//

class GUI{
  GUI(){//constructor
    //Put three buttons and a TextField object in a Frame
    // and make it visible
    Button myButton0 = new Button("Button0");
    TextField myTextField = new TextField("TextField");
    Button myButton1 = new Button("Button1");
    Button myButton2 = new Button("Button2");

    Frame myFrame = new Frame(
                            "Copyright 1998, R.G.Baldwin");
    myFrame.setLayout(new FlowLayout());
    myFrame.add(myButton0);
    myFrame.add(myTextField);
    myFrame.add(myButton1);
    myFrame.add(myButton2);
    myFrame.setSize(300,100);
    myFrame.setVisible(true);

    //MAKE CERTAIN THAT YOU UNDERSTAND THE DIFFERENCE
    // BETWEEN THE FOLLOWING TWO REGISTRATION PROCESSES.
    
    //Instantiate the destination objects that contain the
    // methods to be invoked when the components generate
    // action events.
    abcClass destinationObject0 = new abcClass();
    defClass destinationObject1 = new defClass();

    //Instantiate the action listener object that will take
    // care of dispatching action events from the different
    // sources to the different destination methods.
    MyActionListener actionListener = 
                                   new MyActionListener();

    //The following code registers destination methods on
    // specific sources by informing the actionListener
    // object which method on which destination object
    // is to be invoked whenever one of the buttons 
    // generates an action event.

    //Register different combinations of destination
    // methods and destination objects on sources.
    actionListener.registerDestinationMethod(
                destinationObject0,myButton0,"abcMethod0");
    actionListener.registerDestinationMethod(
              destinationObject0,myTextField,"abcMethod0");
    actionListener.registerDestinationMethod(
                destinationObject0,myButton1,"abcMethod1");
    actionListener.registerDestinationMethod(
                 destinationObject1,myButton2,"defMethod");
                 
    //Force an exception to be thrown by attempting to 
    // register the same source on two different 
    // destinations.
    actionListener.registerDestinationMethod(
                 destinationObject1,myButton0,"defMethod");
    
    //The following code registers the actionListener
    // object on the individual sources so that the 
    // actionListener object will be notified when one 
    // of the sources generates an action event.  In this
    // case, the same actionListener object is being 
    // registered on all of the sources.  The 
    // actionListener object will dispatch the event to
    // the correct destination method and object as
    // defined above.
    myButton0.addActionListener(actionListener);
    myTextField.addActionListener(actionListener);
    myButton1.addActionListener(actionListener);
    myButton2.addActionListener(actionListener);

    //Register listener to terminate program when
    // user closes the Frame.
    myFrame.addWindowListener(new Terminate());
  }//end constructor
}// end class GUI
//=======================================================//

class Terminate extends WindowAdapter{
  public void windowClosing(WindowEvent e){
    System.exit(0);
  }//end windowClosing()
}//end class Terminate
//=======================================================//
-end-