Innovative ways to handle events in AWT and JFC

Learn six different ways to connect event sources with target objects

The Abstract Windowing Toolkit (AWT) and Java Foundation Classes (JFC) use predefined listener interfaces to add behavior to all of their GUI and non-GUI controls. Since event-handling code needs direct access to UI controls that generate events, developers use inner and anonymous classes to implement event-listener interfaces. The JDK 1.1 specification forces compiler implementations to be compatible with the JDK 1.0 class format. For this reason, the compiled units (class files) for inner and anonymous classes tend to consume more bytes than necessary.

This article throws light on the problems inherent in traditional event-handling implementations using AWT/JFC and demonstrates six ways to get around them.

Note: This article assumes that the reader has a thorough understanding of the AWT event model. Experience with the Reflection API is necessary to modify or extend the five utility classes provided in this article.

Event handling in the JDK

Before getting into the guts of the system, let's take a quick peek at the event model mechanism in AWT and JFC.

The event source (typically a UI component) emits events through a predefined listener interface. Any object interested in those events will register an implementation of the listener with the event source through an addXXXListener() method. Except for DocumentEvent, the rest of the events derive from the java.util.EventObject class. This is the essence of the event model; anything else is implementation detail. To get some background on the AWT/JFC event model, see the Resources section at the end of the article.

There are some outstanding issues with current event-handling implementations:

  • Large UI implementations generally result in many anonymous or inner classes.

  • Adapter classes are not defined for many event listener interfaces in JFC. In JDK 1.2, there are 49 listeners and only 9 adapters.

  • Adapters cannot be used if the implementing class has to derive from some other class.

  • Many developers use object diagrams to understand the interconnections between different types of objects. Many IDEs automatically generate object diagrams from the source code. These diagrams help developers understand applications quickly without getting lost in implementation details. Some tools fail to digest inner classes; others generate too many crisscrossing lines due to those same inner classes. Object diagrams for the UI code are difficult to comprehend due to the many connecting lines between event generators (UI controls), event handlers (inner or anonymous classes), and listener interfaces. Most of the time, after generating the object diagrams, developers go back to remove inner class objects from the diagram to improve clarity.

  • Some IDEs generate too much boilerplate code to connect GUI components with event handler methods. Large amounts of glue code results in code bloat and large class files, which is a definite concern for applets.

  • Due to the backward compatibility requirements of compiler specifications, inner and anonymous classes result in a large applet or application footprint.

  • Inner classes cannot derive from adapters if they are already deriving from another base class.

Innovative ways to handle events

The new approach introduced in this article attempts to address the issues listed above by using a few magic classes. I will demonstrate how you can connect event sources and event-handling code using any one of the following six methods:

  • Standard mechanisms:

    • Using anonymous classes

    • Using inner classes

    • Not using inner or anonymous classes

  • New alternatives:
    • EventHandler connects the event source and target object at the object level

    • EventListener connects the event source and target objects at the method level using a predefined listener class

    • GenericListener connects the event source and target objects at the method level by dynamically creating a custom listener implementation at runtime

To improve readability of the article, I have chosen to present a very simple UI implementation. Let's walk through it using a simple program that displays two toggling buttons in a Frame. The program also handles the system-close event to dispose of the Frame. The same example is presented for each of the utility classes listed above. The following three example programs use standard JDK/AWT classes.

ButtonTest1: Using anonymous classes

The following code sample shows how to use anonymous classes to add behavior to buttons and windows. As you can see, action events generated by the buttons are connected to the

bottomButtonAction

method and

topButtonAction

method, respectively. Similarly, the window closing event is handled by calling the

System.exit()

method.

package test.event;

import java.awt.*; import java.awt.event.*;

/** Uses Anonymous Classes to handle events. */ public class ButtonTest1 extends Frame { Button top = new Button("Toggle bottom button"); Button bottom = new Button("Toggle top button");

public ButtonTest1() { super("ButtonTest1 uses Anonymous Classes"); setLayout(new BorderLayout()); add("North", top); add("Center", bottom);

bottom.setEnabled(true); top.setEnabled(false);

bottom.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { bottomButtonAction(e); } });

top.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { topButtonAction(e); } });

this.addWindowListener(new WindowAdapter() { public void windowClosing(ActionEvent e) { System.exit(0); } });

pack(); show(); }

public void topButtonAction(ActionEvent event) { Button b = (Button)event.getSource(); bottom.setEnabled(true); top.setEnabled(false); } public void bottomButtonAction(ActionEvent event) { Button b = (Button)event.getSource(); bottom.setEnabled(false); top.setEnabled(true); } public static void main(String[] args) { new ButtonTest1(); } }

Disadvantages

If the event-handler code happens to contain large code blocks, it reduces the readability of the program. For such cases, this approach is not a good choice.

This approach also generates anonymous inner classes (the total compiled class size is high). In general, the logic behind the event handling should be extremely simple and small so as to avoid maintenance and readability issues.

Advantages

This is a good choice in cases in which the event-handling code is extremely small. This approach improves code readability, as developers can see the event-handler implementation when the listener is added to the UI control.

ButtonTest2: Using inner classes

This example reimplements the same program using inner classes instead of anonymous classes. The following three classes wrap the event-handling code found inside the anonymous classes:

  • bottomButtonActionHandler
  • topButtonActionHandler
  • windowEventHandler

package test.event;

import java.awt.*; import java.awt.event.*;

/** Uses Inner Classes to handle events. */ public class ButtonTest2 extends Frame { Button top = new Button("Toggle bottom button"); Button bottom = new Button("Toggle top button");

public ButtonTest2() { super("ButtonTest2 uses Inner Classes"); setLayout(new BorderLayout()); add("North", top); add("Center", bottom);

bottom.setEnabled(true); top.setEnabled(false);

bottom.addActionListener(new bottomButtonActionHandler()); top.addActionListener(new topButtonEventHandler()); this.addWindowListener(new windowEventHandler());

pack(); show(); }

class topButtonEventHandler implements ActionListener { public void actionPerformed(ActionEvent event) { Button b = (Button)event.getSource(); bottom.setEnabled(true); top.setEnabled(false); } }

class bottomButtonActionHandler implements ActionListener { public void actionPerformed(ActionEvent event) { Button b = (Button)event.getSource(); bottom.setEnabled(false); top.setEnabled(true); } }

class windowEventHandler extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } }

public static void main(String[] args) { new ButtonTest2(); } }

Disadvantages

Object diagrams become ugly due to the presence of numerous inner classes and lines connecting them to several classes and interfaces. This approach results in a large applet/application footprint, as was also the case in the previous example.

Advantages

This approach is preferred to the previous method because it scales well to handle more events using inner classes.

ButtonTest3: Not using inner or anonymous classes

In this example, the main class itself implements all listeners for all UI components contained in the container.

package test.event;

import java.awt.*; import java.awt.event.*;

/** Uses Inner Classes to handle events. */ public class ButtonTest3 extends Frame implements ActionListener,WindowListener { Button top = new Button("Toggle bottom button"); Button bottom = new Button("Toggle top button");

public ButtonTest3() { super("ButtonTest3 does not use Anonymous or Inner Classes"); setLayout(new BorderLayout()); add("North", top); add("Center", bottom);

bottom.setEnabled(true); top.setEnabled(false);

bottom.addActionListener(this); top.addActionListener(this); this.addWindowListener(this);

pack(); show(); }

/** This approach has potential to result in huge if-else blocks. */ public void actionPerformed(ActionEvent event) { Button b = (Button)event.getSource(); if(b == bottom) { bottom.setEnabled(false); top.setEnabled(true); } else if (b == top) { bottom.setEnabled(true); top.setEnabled(false); } }

public void windowClosing(WindowEvent e) { System.exit(0); }

public void windowActivated(WindowEvent e){} public void windowClosed(WindowEvent e){} public void windowDeactivated(WindowEvent e){} public void windowDeiconified(WindowEvent e){} public void windowIconified(WindowEvent e){} public void windowOpened(WindowEvent e){}

public static void main(String[] args) { new ButtonTest3(); } }

Disadvantages

Compared to first two approaches, this approach is easy to learn and use. Novice programmers tend to abuse this approach to a great extent as they try to accommodate more event handlers in a single class. This approach uses the

if-else

block. If the UI happens to contain lots of controls, you will end up with a huge

if-else

block. The code results in non-object-oriented spaghetti code and it does not scale well with a number of GUI components. The object diagrams become cluttered, with many lines connecting this class to several interfaces. Also, large UI implementations tend to be difficult to maintain. As the containing class derives from a container class (

Frame

or

Panel

) to contain controls inside, it cannot use any of the adapter classes provided in the JDK. This results in many empty methods as the above example demonstrates.

Advantages

This code does not generate inner or anonymous classes. As a result, the total number of bytes consumed by class files is relatively small compared to the first two approaches.

New ways to handle events

The next three examples use the following utility classes:

  • EventHandler
  • EventListener
  • GenericListener

These utility classes are included in the zip file listed in the Resources section at the end of this article.

The zip file also contains the following classes, which are useful for developing JFC-based user interfaces:

  • JEventHandler
  • JEventListener

To improve ease of use, I have defined the above classes in the java.awt.event and javax.swing.event packages. This way, you won't need to add new import statements to use the classes provided in this article.

The following API is used to rewrite the example presented earlier:

public boolean EventHandler.addXXXListener( Object
eventSource, Object eventTarget);

The EventHandler class implements this static method to connect event source and target objects together for AWT-based UI controls. The JEventHandler class derives from the EventHandler class to implement the same functionality for JFC-based UI controls.

public EventListener (

Object eventTarget,

String sTargetMethod,

String sInterfaceMethod);

The EventListener class implements this constructor to connect event source and target objects together for AWT-based UI controls. The JEventListener class derives from the EventListener class to implement the same functionality for JFC-based UI controls.

public boolean static GenericListener.connect(

Object eventSource,

Object eventTarget,

String sInterfaceMethod,

String sTargetMethod );

ButtonTest4: Using EventHandler

You should use the static methods provided in this class to connect the event source and target object together. This class implements static methods to handle all types of AWT event listeners. Internally, this class uses a single implementation object to register which object is interested in which type of event from which event source. It attaches a listener to the event source you passed in. Whenever an event is received through the listener, it broadcasts the events to all the target objects that are interested in receiving the event. At the time of broadcast, it uses the Reflection API to find a matching listener method signature on the target object. If an object is interested in receiving an action event, for example, then

EventHandler

expects an implementation of the

actionPerformed (ActionEvent ae)

method in the target object. If such method is not found on the target, then the broadcast will have no effect on the target object. All it means is that the target object is not interested in providing an implementation to handle the event. This is quite handy for developers who are

only

interested in implementing a particular method in a event listener.

EventHandler

gives you this flexibility by not allowing you to implement all methods in the event listener and still be able to receive events. Initially, this might look like magic, but believe me, this trick is possible due to the power of the Reflection API.

The following program reimplements our toggle button example using the EventHandler class.

package test.event;

import java.awt.*; import java.awt.event.*;

/** Uses EventHandler class. */ public class ButtonTest4 extends Frame { Button top = new Button("Toggle bottom button"); Button bottom = new Button("Toggle top button");

public ButtonTest4() { super("ButtonTest4 uses EventHandler"); setLayout(new BorderLayout()); add("North", top); add("Center", bottom);

bottom.setEnabled(true); top.setEnabled(false);

EventHandler.addActionListener(top, this); EventHandler.addActionListener(bottom, this); EventHandler.addWindowListener(this, this); pack(); show(); } public void windowClosing(WindowEvent e) { System.exit(0); }

public void actionPerformed(ActionEvent event) { Button b = (Button)event.getSource(); if(b == bottom) { bottom.setEnabled(false); top.setEnabled(true); } else { bottom.setEnabled(true); top.setEnabled(false); } } public static void main(String[] args) { new ButtonTest4(); } }

Disadvantages

This approach does not scale well for large UI containers. As you can see, the container object (

this

) is passed to the

addActionListener()

method as a target object.

Frames

or

Panels

that contain a lot of UI controls can optionally use inner classes to implement methods defined in event listeners and register these inner class objects as target objects. The small advantage here is that the inner class can derive from any class other than the

EventAdapter

class.

Advantages

This approach is good for small, simple programs. There is no need to implement any interface or extend any adapter. It results in the smallest possible compiled code size.

ButtonTest5: Using EventListener

This example uses the

EventListener

object as a valid listener to attach an event-handling method to all AWT-based UI controls. The

EventListener

class implements all event listener interfaces. Thus, it is always a valid argument to any

addXXXListener()

methods.

Since EventListener implements all possible listener interfaces in AWT, it is able to receive all predefined events. Those events will be forwarded to the target object. You have the choice of hooking any method that accepts that event as an argument. Unlike the EventHandler class, EventListener is unforgiving; you have to implement the target method specified as an argument to the constructor. The EventListener class constructor throws an exception if that method does not exist on the target object. You can remove this constraint if doing so would be more suitable to your development framework.

package test.event;

import java.awt.*; import java.awt.event.*; import java.util.*; import java.awt.event.EventListener;

/** Uses EventListener class. */ public class ButtonTest5 extends Frame { Button top = new Button("Toggle bottom button"); Button bottom = new Button("Toggle top button");

public ButtonTest5() { super("ButtonTest5 uses EventListener"); setLayout(new BorderLayout()); add("North", top); add("Center", bottom);

bottom.setEnabled(true); top.setEnabled(false);

top.addActionListener(new EventListener(this, "button1Action","actionPerformed")); bottom.addActionListener(new EventListener(this, "button2Action", "actionPerformed" )); this.addWindowListener(new EventListener(this, "windowClosingMethod", "windowClosing" ));

pack(); show(); } public void windowClosingMethod(WindowEvent e) { System.exit(0); }

public void function(ActionEvent event) { Button b = (Button)event.getSource(); if(b == bottom) { bottom.setEnabled(false); top.setEnabled(true); } else { bottom.setEnabled(true); top.setEnabled(false); } }

public void button1Action(ActionEvent e) { function(e); }

public void button2Action(ActionEvent e) { function(e); }

public static void main(String[] args) { new ButtonTest5(); } }

In the above example, the topbuttonAction and bottombuttonAction methods on the container object are connected with the actionPerformed method expected on the action event listener for the top button and the bottom button, respectively. If necessary, these methods can be implemented on any class other than the container class.

Disadvantages

EventHandler

and

EventListener

classes are predefined to work with AWT only. They can not be used to receive events from any other event sources.

Advantages

This method allows users to map a target method for each event generated on an event source. It avoids huge

if-else

blocks, and it scales well with any number of GUI components. With this approach, there is no need to use inner or anonymous classes. It results in a small class file footprint, and can be used with IDEs to reduce the total footprint of GUI classes.

ButtonTest6: Using GenericListener

The

GenericListener

utility class, developed by engineers at JavaSoft, can handle any type of listener. It is not limited to AWT and JFC. To get a sense of its full potential, take a look at

Demo.java

. My example here is a simple utility method written on top of the code Sun's engineers have already provided. For your convenience, the source code for this implementation is included in the zip file listed in the

Resources

section below.

Please note that the source included in this article has been modified to work with JDK 1.2 with the optimizer switch turned on. (By the time this article went live, Sun engineers had decided to include proxy class generation functionality in the JDK 1.3 prerelease. See Resources for more details.)

package test.event;

import java.awt.*; import java.awt.event.*; import java.util.*;

public class ButtonTest6 extends Frame { Button top = new Button("Toggle bottom button"); Button bottom = new Button("Toggle top button");

public ButtonTest6() { super("ButtonTest6 uses Generic Listener"); setLayout(new BorderLayout()); add("North", top); add("Center", bottom);

bottom.setEnabled(true); top.setEnabled(false);

GenericListener.connect(top, this,ActionListener.class, "actionPerformed", "button1Action"); GenericListener.connect(bottom,this,ActionListener.class, "actionPerformed", "button2Action"); GenericListener.connect(this,this,WindowListener.class, "windowClosing", "windowClosingMethod");

pack(); show(); } public void windowClosingMethod(WindowEvent e) { System.exit(0); }

public void function(ActionEvent event) { Button b = (Button)event.getSource(); if(b == bottom) { bottom.setEnabled(false); top.setEnabled(true); } else { bottom.setEnabled(true); top.setEnabled(false); } }

public void button1Action(ActionEvent e) { function(e); }

public void button2Action(ActionEvent e) { function(e); }

public static void main(String[] args) { new ButtonTest6(); } }

Since GenericListener generates the actual listener implementation at runtime, you must provide the class object for the listener in which you are interested. In the above example, we are passing the ActionListener.class and WindowListener.class as arguments to the connect method on the GenericListener class. connect() is a static method that uses the GenericListener implementation internally to create the listener class at runtime. These classes will be loaded through the class loader only once for each listener-event combination.

Advantages

This approach allows users to map a target method for each event generated on an event source.

GenericListener

gives us all the benefits of

EventListener

. Since it is not predefined to work with AWT or JFC, you can use this approach to work with any event-generating source (including non-UI sources). It avoids huge

if-else

blocks, and it scales well with any number of GUI components. There is no need to use inner or anonymous classes. It results in a small class file footprint, and can be used with IDEs to reduce total footprint of GUI classes. Listeners are created and loaded at runtime. In addition, IDEs and bean environments can use this approach to hook up components at runtime.

The total footprint for each of the above examples is listed in the table below. The table shows the bytes occupied by the class files for all the examples I have discussed so far. Notice that ButtonTest4 has the smallest footprint among all examples.

Comparison of example techniques
Example nameFootprint (in bytes)Remarks
ButtonTest13,253Uses anonymous class
ButtonTest23,847Uses inner class
ButtonTest31,759Main class implements all interfaces
ButtonTest41,391Uses EventHandler class
ButtonTest51,679Uses EventListener class
ButtonTest62,169Uses GenericListener class

Advantages with the extended approach

The

EventHandler

and

EventListener

are easy to use in comparison with standard approaches. There is no need to import, use, extend, or implement any interfaces or event adapters. From the table above, it is clear that the total footprint of the compiled classes is smaller when we use

EventHandler

and

EventListener

. The space taken up by the example that uses

GenericListener

is a little larger, but still smaller than the first two examples. Object diagrams for Java projects will be more readable due to the appearance of fewer inner classes. IDEs can use these utility classes to generate code that is easy to read and maintain.

Future work

Currently, the

EventHandler

class implements two modes of execution:

  • Up-front full method caching: In this mode, event delivery is fast, but the program consumes more memory.

  • No method caching: In this mode, method references are not cached, so a performance hit will be taken for every event notification. This is a bit slower than the previous mode, but it consumes less memory.

An interested soul can implement adaptive method caching. In this mode, method references that can be cached as events are generated. This mode is certainly better than either no caching or full caching.

Smaller UI implementations

This articles sums up six possible ways to handle events in AWT and JFC, and discusses the advantages and issues with each approach. You should understand that no one method is good for all situations. Care should be taken in selecting the event-handling method while developing large, UI-extensive applications. As shown above,

EventHandler

,

EventListener

, and

GenericListener

classes enable you to create compact UI implementations with a minimal amount of code.

Eliminating inner classes will lead to non-object-oriented programming. It should not be attempted unless the benefits are profound (if you develop large applets with lots of UI and want to reduce the download time, for example). The new utility classes offered in this article may be of more interest to developers who are trying to reduce class size or who need to connect objects at runtime.

Satheesh A. Donthy received his Masters degree from I.I.T. Kharagpur in India. He has been developing software using object-oriented systems since 1992. After working for four years at Intergraph developing CAD/CAM applications, he switched to consulting. Since 1996, he has been consulting in and around the Boston area with the Universal Software Corporation. Satheesh has worked on many exciting Java projects as an architect and lead developer. He is the author of Source Explorer for Java, a complete source code and bytecode browsing system. Currently, he is working with the Netscape Professional Services group to help companies deploy ecommerce products.

Learn more about this topic

Papers that inspired me to write this article

JavaSoft-published information about dynamic proxy implementation

Good introductory material for the AWT event model

Good introductory material for the Reflection API

Join the discussion
Be the first to comment on this article. Our Commenting Policies