"Keep listening for upcoming events"

How to wire JavaBeans together using "event listeners"

In Douglas Adams's The Hitchhiker's Guide to the Galaxy, Arthur Dent has a problem:

"You know," said Arthur, "it's at times like this, when I'm trapped in a Vogon airlock with a man from Betelgeuse, and about to die of asphyxiation in deep space, that I really wish I'd listened to what my mother told me when I was young."

"Why, what did she tell you?"

"I don't know, I didn't listen."

JavaBeans can be configured to "listen" to other software objects. And as you'll see, a Java 1.1 class (including any JavaBean) can listen not only to its parent, but also to any class that produces events by becoming an event listener. The event listener idea is central to how Java classes (and other JavaBeans) handle events.

Last month we discussed a specific type of event listener: the PropertyChangeListener interface that takes action when other Beans' properties change. This month we'll take a closer look at the whole concept of an "event listener." You'll see how event listeners are used in the new AWT. (When I say "new," I mean since JDK 1.1 was released.) We'll talk about how to define your own event types, and then make those new event types visible to other classes. Then, as an example, we'll extend the BarChartBean, creating a new event type and then using it to wire the BarChartBean to another Bean. We'll go over some details about how to write event listener interfaces, using the AWT as an example, and conclude with a discussion of inner classes, a new Java 1.1 language feature.

I'd also like to introduce two new icons that will help identify key points:

The JavaBean icon is a key concept for JavaBeans.
And the cuppajoe icon indicates new or key ideas specific to the Java language.

What's an event?

A software event is a piece of data that indicates that something has occurred. Maybe a user moved the mouse. Maybe a datagram just arrived from the network. Maybe a sensor has detected that someone's just come in the back door. All of these occurrences can be modeled as events, and information about what happened can be included in the event. Often it's convenient to write software systems in terms of event processing: Programming then becomes a process of specifying "when this happens, do that." If the mouse moved, move the cursor with it; if a datagram arrived, read it; if there's an intruder, play the recording of Roseanne releasing the rabid Rottweiler.

Usually, an event contains information about the event source (the object that created or first received the event), when the event occurred, and some subclass-specific information that the event receiver can use to figure out what happened and what to do. In a windowing system, for example, there might be a subtype of event for mouse clicks. The mouse click event would include the time the click occurred, and also might include such information as where on the screen the mouse was clicked, the state of the SHIFT and ALT buttons, and an indication of which mouse button was clicked. The code that handles the event is called, strangely enough, the event handler.

So, what does this have to do with JavaBeans? Events are the primary way that Beans communicate with each other. As we'll see below, if you choose events and their connections judiciously, you can interconnect Beans in your application, tell each Bean what to do in response to the events it cares about, and the application simply behaves. Each Bean minds its own business, responding appropriately to incoming events, and sending new events to interested neighboring Beans as new situations occur. Once you understand how to use events, you can write Beans that use them to communicate with other components. And what's more, external systems like Integrated Development Environments (IDEs) automatically detect the events your Beans use and let you interconnect Beans graphically. IDEs also can send events to and receive events from JavaBeans, essentially controlling them from the outside.

In order to understand how events work with Beans, though, you've got to understand how they work in Java. And events work differently now that JDK 1.1 is the standard.

Old News: What's wrong with JDK 1.0 events?

In the Java Developer's Kit (JDK) 1.0, events were used primarily in the Abstract Windowing Toolkit (AWT) to tell classes when something happened in the user interface. Programmers used inheritance to process events by subclassing an object that could receive that event type and overriding how the parent class processed the event.

For example, in Java version 1.0, the only way to get an Action event was to inherit from some other class that already knew how to handle Action events:

public class MyButton extends java.awt.Button
{
    // Override action() method to handle action event
    public boolean action(Event evt, Object what) {
        // Now do something with the action event
    }
}

This means that only classes that inherited from java.awt.Button could do anything to respond to a button click. This structure left Java events tied to the user interface and inflexible. It wasn't easy to create new event types. And even if you could create new events, it was hard to change the events that a class could respond to, because that information was hard-coded into the "family tree" (the inheritance graph) of the AWT.

The new JDK 1.1 has a more general event structure, which allows classes that produce events to interact with other classes that don't. Instead of defining event-processing methods that client subclasses must override, the new model defines interfaces that any class may implement if it wants to receive a particular message type. (You may hear this referred to as processing events by delegation instead of by inheritance.) Let's continue with this JDK 1.0 button example and move toward the JDK 1.1 model.

Let's say I wanted to create a new class that does something when a button is pressed. In 1.0, I'd have to subclass java.awt.Button to handle the Button action event, and then that button would somehow notify my new class that the button press had occurred:

//... elsewhere in the program we define the object that "listens"
// for button actions.
ActionListener myActionListener = new ActionListener();
//...
// Button action event
public class MyButton extends java.awt.Button
{
    // Override action() to notify my new class
    public boolean action(Event evt, Object object)
    {       myActionListener.action(evt, object);
    }
}

Now the object myActionListener receives an event every time any MyButton is pushed. myActionListener is not necessarily a subclass of java.awt.Component, but it does contain an action() method. We call the new class an ActionListener because, seen from the new class's point of view, it is "listening" for action events on the button it is "attached" to. There are still some problems, though:

  • The object to notify when this button gets pushed is hard-coded, meaning I can't "rewire" the association between the Button and the myActionListener at runtime.

  • Only a single other object gets notified: What if several other objects have an interest in the button press?

  • We still haven't solved the problem that receiving a button action event is inherited -- meaning that myActionListener must inherit from some base class that "knows" about buttons and their action events.

A solution to the first problem might be to add to MyButton the methods setListener(ActionListener newListener) and myNewClass getListener(), and that way be able to change the object that gets notified. Unfortunately, we'd still be unable to associate only one object per button, so let's say we make it a list of "listeners" instead:

// Button action event
public class MyButton extends java.awt.Button
{   private Vector listeningObjects = new Vector();
    // Override action() to notify my new class
    public boolean action(Event evt, Object object)
    {      for (int i = 0; i < targetList.length; i++)
          ((ActionListener)(listeningObjects.elementAt(i))).action(evt, object);
    }   public void addActionListener(ActionListener newListener);
    {
       listeningObjects.addElement(newListener);
    }
    public void removeActionListener(ActionListener newListener);
    {
       listeningObjects.removeElement(newListener);
    }
}

Now, any instance of ActionListener can "listen" for events on any instance of MyButton by calling addActionListener(this), and can stop listening by calling removeActionListener(this). That's all fine, but we're still stuck with the inheritance problem: Only Buttons and ActionListener objects (and their descendents) can receive button action events. Java has a novel solution for this problem: the interface.

Interfaces and event listeners

The Java glossary provides the following definition:

interface: In Java, a group of methods that can be implemented by several classes, regardless of where the classes are in the class hierarchy.

Why would such a beast be useful?

An interface defines a "role" that any class may choose to play by implementing a set of operations that the interface defines.

An interface definition looks like a class definition:

// Still in JDK1.0
public interface ActionListener
{
    void action(Event evt, Object object);
}

Any class that wants to be a ActionListener simply has to define an action function, and then declare that it implements the ActionListener interface by using the implements keyword:

public class SomeRandomClass extends SomeOtherClass implements ActionListener
{   // Implement ActionListener methods
    void action(Event evt, Object object)
    {
        // Do whatever
    }
    SomeRandomClass()
    {
        super();
        // Blah blah blah...
    }
    // ... Continue with implementations of SomeRandomClass methods
}

This is extremely cool, because with interfaces, you're no longer locked into a strict single-inheritance hierarchy.

In object-oriented parlance, inheritance often is called an ISA relationship: The class Person inherits Mammal because a Person ISA (is a) Mammal. Association of objects, wherein objects maintain references or pointers to one another, is sometimes called a HASA relationship: an Automobile HASA Crankshaft.

The interface construct adds to object-oriented thinking the concept of ACTS-AS-A. In this case, any class ACTSASA ActionListener simply by implementing the interface.

Every method in an interface is by definition abstract, meaning no implementation exists for the method (nor can it). So, if your class extends an interface, it must provide some implementation for every method defined in the interface.

The JDK 1.1 AWT defines interfaces for processing events, and the AWT user-interface elements provide the addEventtypeListener and removeEventTypeListener shown above. java.awt.Button in 1.1, for instance, has the methods:

void addActionListener(ActionListener listener)
void removeActionListener(ActionListener listener)

This means any ActionListener may add itself to the list of objects listening for Action events. A class qualifies as an ActionListener by implementing the ActionListener interface:

public interface ActionListener extends EventListener {
      public void actionPerformed(ActionEvent e)
}

Classes that want to receive events no longer have to inherit ActionListener -- they can just implement it and play the role of ActionListener to some other object.

The single argument of the actionPerformed method, ActionEvent, is a class derived from java.util.EventObject. It provides several useful functions that help the event listener figure out who sent the event, what the state of the SHIFT and ALT keys were at the time the event occurred, and so on. Check out the full capabilities of these events in the online documentation (see Resources section below).

Also note that the interface extends EventListener, which itself is an interface with no methods! Requiring event listeners to extend the empty java.util.EventListener interface lets programs (especially IDEs) manipulate EventListeners of various subtypes in an abstract way (maintaining lists of EventListeners, for example).

1 2 3 Page 1
Page 1 of 3