How do I create my own events to pass between objects?

Scott Stanchfield

Event firing in Java requires the following elements

  • An event class to transmit information about the event
  • An interface that describes how the bean will notify interested parties, known as listeners
  • A data structure to track listeners
  • Registration methods to add and remove listeners
  • Code to fire the events

For example, suppose that you were creating a WeatherStation bean that notifies any interested parties when the sun rises or sets. This example is used throughout this FAQ.

An Event Class

The first requirement for event firing is a class to represent information about the event. The SunEvent object contains the time of the event and a boolean property that tells us if the event represents the sun rising or setting.

Event classes should be immutable. Immutable objects are ones that cannot be changed. Event firing passes a reference to a single event instance to each listener in an unspecified order. It is important that those listeners cannot modify the event, or it could change the way processing continues for other listeners. Note that the following class is not a bean, because its superclass does not implement java.io.Serializable. If you like, you can define the event object as a bean, but in practice this is rarely done.

Note that the event object must extend class java.util.EventObject. EventObject provides the getSource() method so you can determine the event origin. By extending EventObject, those objects can be handled generically in other methods, and some tools, like VisualAge for Java, take advantage of this when creating generic methods to forward events.

import java.util.Date;
import java.util.EventObject;

/**
 * An event that represents the Sun rising or setting
 */
public class SunEvent extends EventObject {
  private boolean risen;
  private Date date;
  
  public SunEvent(Object source, boolean risen, Date date) {
    super(source);
    this.risen = risen;
    this.date = date;
  }
  
  /** return a String representation of the date */
  public String getDate() {
    // return only a String representation
    //   so the user cannot modify the real date
    return date.toString();
  }
  
  /** return whether the sun rose or set */
  public boolean isRisen() {
    return risen;
  }
}

EventObject requires a source (a reference to the object that fired the event), which is set to the WeatherStation bean when firing the event. Whenever events are fired, an instance of the EventObject class is created and passed to each registered listener.

An Event-Listener Interface

Event listeners are classes that have registered interest in an event that a bean can fire. The bean notifies listener classes by calling certain methods in those listener classes. But how does the bean know which methods to call?

When defining the event, you also define a contract between the event source and its listeners. This contract specifies which methods the event source will call. Any listeners must implement those methods. Sounds like a perfect use for a Java language interface.

Therefore, define an interface that specifies the contract. Because each listener must implement that interface, the event-source bean can determine which methods it can call. Continuing the example, define a simple interface for the sun events.

Note that the following interface extends the java.util.EventListener interface. EventListener requires no methods. It is just a tag that indicates an interface is acting as an event listener. This assists determination of the events a bean can fire.

import java.util.EventListener;

/** A contract between a SunEvent source and
  *   listener classes
  */
public interface SunListener extends EventListener {
  /** Called whenever the sun changes position
    *   in a SunEvent source object 
    */
  public void sunMoved(SunEvent e);
}

Any SunEvent listeners are required to implement a single method, sunMoved(). Normally, event-listener methods should take a single parameter: the event object. However, if you have a strong need, the bean specification allows (but strongly discourages) different parameters.

An Event-Source Bean

Now you define the source of the sun events, the WeatherStation. This class needs to track its listeners and fire the events. This is implemented as follows, and a simple GUI to trigger the SunEvents is provided:

import java.util.Vector;
import java.util.Date;
import java.util.Enumeration;
import java.awt.Button;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.Serializable;

/** A sample event source - this class fires SunEvents
  *   to anyone watching. A simple GUI is provided
  *   with "rise" and "set" buttons that cause the
  *   SunEvents to be fired.
  */
public class WeatherStation extends Frame 
                            implements Serializable {
  private transient Vector listeners;

  /** Provide a simple GUI that triggers our SunEvents
    */
  public WeatherStation() {
    super("Sun Watcher");
    setLayout(new GridLayout(1,0));
    Button riseButton = new Button("Rise");
    Button setButton = new Button("Set");
    add(riseButton);
    add(setButton);

    // make the "Rise" button fire "rise" SunEvents
    riseButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        fireSunMoved(true);
      }
    });

    // make the "Rise" button fire "set" SunEvents
    setButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        fireSunMoved(false);
      }
    });

    // Provide a means to close the application
    addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    
    pack();
  }

  /** Register a listener for SunEvents */
  synchronized public void addSunListener(SunListener l) {
    if (listeners == null)
      listeners = new Vector();
    listeners.addElement(l);
  }  

  /** Remove a listener for SunEvents */
  synchronized public void removeSunListener(SunListener l) {
    if (listeners == null)
      listeners = new Vector();
    listeners.removeElement(l);
  }

  /** Fire a SunEvent to all registered listeners */
  protected void fireSunMoved(boolean rose) {
    // if we have no listeners, do nothing...
    if (listeners != null && !listeners.isEmpty()) {
      // create the event object to send
      SunEvent event = 
        new SunEvent(this, rose, new Date());

      // make a copy of the listener list in case
      //   anyone adds/removes listeners
      Vector targets;
      synchronized (this) {
        targets = (Vector) listeners.clone();
      }

      // walk through the listener list and
      //   call the sunMoved method in each
      Enumeration e = targets.elements();
      while (e.hasMoreElements()) {
        SunListener l = (SunListener) e.nextElement();
        l.sunMoved(event);
      }
    }
  }
}

The bold text provides the support for tracking listeners and calling their event-handler methods.

The event source can now notify interested listeners. The synchronized keywords and cloning are necessary to avoid possible problems if the list of listeners changes while you are notifying listeners. Pressing the Rise and Set buttons fires an appropriate SunEvent to any registered listeners.

Typically, developers will create a fire method similar to the previous fireSunMoved() method, but the method is not required. The event-firing code can appear in any method you prefer.

A Sample Event Listener

In the example, the weather channel informs any interested parties about the sun rising or setting. Continuing this example, Mrs. Jones assigns her class the task of graphing the behavior of the sun. They must watch a weather channel to find out at exactly what time the sun rose and set each day, logging this information in their notebook. This example can be modeled using a Student class as follows:


/** A sample SunListener, logging when the sun rises and sets
  */
public class Student implements SunListener {
  private String name;

  /** constructor -- get the student's name */
  public Student(String name) {
    this.name = name;
  }

  /** sunMoved method comment. */
  public void sunMoved(SunEvent e) {
    log(name + "	logs : " +
        (e.isRisen() ? "rose" : "set") +
        " at " + e.getDate()); 
  }

  /** A simple log method - just print the text */
  protected void log(String text) {
    System.out.println(text);
  }
}

Note that Student implements SunListener, defining the details of a sunMoved() method. Any number of Students may watch the weather channel to hear the time of sunrise and sunset.


/**
 * A simple test of the SunEvent source and listeners
 */
public class WeatherTest {
  /** Run a test on the weather station, using Scott's
   *    kids as sample students
   */
  public static void main(String[] args) {
    // create a new sun event source
    WeatherStation w = new WeatherStation();

    // add some students to listen for sun rise/set
    w.addSunListener(new Student("Nicole"));
    w.addSunListener(new Student("Alex"));
    w.addSunListener(new Student("Trevor"));
    w.addSunListener(new Student("Claire"));

    // display the GUI for the weather channel
    w.setVisible(true);
  }
}

Running WeatherTest and pressing the Rise and Set buttons results in something like the following:


Nicole logs : rose at Thu Apr 29 14:55:21 PDT 1999
Alex   logs : rose at Thu Apr 29 14:55:21 PDT 1999
Trevor logs : rose at Thu Apr 29 14:55:21 PDT 1999
Claire logs : rose at Thu Apr 29 14:55:21 PDT 1999
Nicole logs : set at Thu Apr 29 14:55:22 PDT 1999
Alex   logs : set at Thu Apr 29 14:55:22 PDT 1999
Trevor logs : set at Thu Apr 29 14:55:22 PDT 1999
Claire logs : set at Thu Apr 29 14:55:22 PDT 1999

Warning

The above listener methods run in a specific order each time an event is fired. This is because we stored the listeners in an ordered data structure. There is no guarantee of event-notification order in the bean specification! Unless a bean documents its event-firing behavior as ordered, you should not assume any particular order of notification. Beans could store their listeners in any data, and they may report them in a different order every time they fire an event.

Also note that event firing is synchronous. That is to say, the event source calls only one event handler at a time. Most beans fire their events in this manner (although there is nothing to stop you from writing a bean that fires events asynchronously). Because of this, you should perform only short operations in your event handlers. Think of it this way: The next listener has to wait for you, so you should be courteous and return as quickly as possible. If you need more complex operations, spawn another thread from your event handler.

Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.