September 18, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Typed and Targeted Property Change Events in Java

  • January 29, 2007
  • By Garret Wilson
  • Send Email »
  • More Articles »

Listening for bound property changes of a JavaBean is simple enough, and determining which bean fired the event is as straightforward as calling PropertyChangeEvent.getSource(). But what to do if the JavaBean in question has complex property values, each of which can have its own bound properties? This article examines a method for representing targeted events—those that may represent changes in objects other than the source. It also introduces an adapter that allows type-aware property change events using Java 5.0 generics. Examples are provided using the Guise™ Internet application framework.

Typed Property Change Events

When Sun added the JavaBeans component architecture, Java acquired a standard way to conceptualize properties of objects that were richer than mere data fields. Besides the ability to perform custom actions when properties are read and/or written, using so-called getter and setter methods, respectively, Java allowed third-party observer objects to listen for property changes and perform their own actions in response. Such properties which notify listeners of changes are referred to as bound properties. The foundational classes of this architecture are found in the java.beans package.

The two fundamental classes that implement bound properties in JavaBeans are PropertyChangeListener and PropertyChangeEvent. Sun provided the PropertyChangeSupport class to assist developers in creating classes with bound properties, obviating the need for a class to keep track of its own property change listeners and to manually fire property change events to those listeners. Using these classes, a simple Automobile class could implement an engine bound property, assuming that an Engine class exists. (Note that we require that an engine always be non-null to simplify the logic. Without this requirement, the code would need extra checks for null throughout.)

import java.beans.*;

public class Automobile
{
  protected final PropertyChangeSupport propertyChangeSupport;
  private Engine engine;

  /**Default constructor with a default engine.*/
  public Automobile()
  {
    propertyChangeSupport=new PropertyChangeSupport(this);
    engine=new Engine();
  }

  /**@return The car's current engine.*/
  public Engine getEngine() {return engine;}

  /**Sets the car's engine.
  @param newEngine The new engine.
  @exception NullPointerException if the engine is
      <code>null</code>.
  */
  public void setEngine(final Engine newEngine)
  {
    if(newEngine==null)
    {
      throw new NullPointerException("No engine provided.");
    }
    if(!engine.equals(newEngine))
    {
      final Engine oldEngine= engine;
      engine=newEngine;
      propertyChangeSupport.firePropertyChange("engine",
                                               oldEngine,
                                               newEngine);
    }
  }

  /**Add a property change listener for a specific property.
  @param propertyName The name of the property to listen on.
  @param listener The <code>PropertyChangeListener</code>
      to be added.
  */
  public void addPropertyChangeListener(final String propertyName,
      final PropertyChangeListener listener)
  {
    propertyChangeSupport.addPropertyChangeListener(propertyName,
                                                    listener);
  }

  /**Remove a property change listener for a specific property.
  @param propertyName The name of the property that was listened on.
  @param listener The <code>PropertyChangeListener</code>
      to be removed
  */
  public void removePropertyChangeListener(final String propertyName,
      final PropertyChangeListener listener)
  {
    propertyChangeSupport.removePropertyChangeListener(propertyName,
                                                       listener);
  }
}

An interested class could listen for the car's engine changing by adding a property change listener, usually an anonymous inner class:

final Automobile automobile=new Automobile();
automobile.addPropertyChangeListener("engine",
                                     new PropertyChangeEvent()
    {
      public void propertyChange(
          final PropertyChangeEvent propertyChangeEvent)
      {
        final Engine newEngine=
            (Engine)propertyChangeEvent.getNewValue();
        if(newEngine.getSize()>8)
        {
          Systen.out.println("That's a big engine.");
        }
      }
    });

That's easy enough. But the old and new values of PropertyChangeEvent are always expressed as Object, forcing us to cast those values, even when we know ahead of time what type to expect. When we listen for the "engine" property, we expect the value to be an Engine. Wouldn't it be nice if PropertyChangeEvent used generics, so that its values were automatically returned as the expected types?

Unfortunately, PropertyChangeEvent was created long before generics were added to the Java language in version 5.0, but using another Java feature, covariance, it's possible to create an adapter class that provides us with typed values while providing backwards-compatibility with PropertyChangeEvent. This part of the task is surprisingly easy; here is the basic structure of a class, com.garretwilson.beans.GenericPropertyChangeEvent<V>, that does just that:

/**A property value change event is a Java Beans property change
    event retrofitted to use generics to cast to proper value type.
@param <V> The type of property value.
@author Garret Wilson
*/
public class GenericPropertyChangeEvent<V>
    extends PropertyChangeEvent
{

  public GenericPropertyChangeEvent(final Object source,
      final String propertyName, final V oldValue, V newValue)
  {
    super(source, propertyName, oldValue, newValue);
  }

  @SuppressWarnings("unchecked")
  public GenericPropertyChangeEvent(final PropertyChangeEvent
      propertyChangeEvent)
  {
    this(propertyChangeEvent.getSource(),
         propertyChangeEvent.getPropertyName(),
         (V)propertyChangeEvent.getOldValue(),
         (V)propertyChangeEvent.getNewValue());
    setPropagationId(propertyChangeEvent.getPropagationId());
  }

  @SuppressWarnings("unchecked")
  public V getOldValue()
  {
    return (V)super.getOldValue();
  }

  @SuppressWarnings("unchecked")
  public V getNewValue()
  {
    return (V)super.getNewValue();
  }
}

So far the main functionality of the class is to cast the old and new values to the generic type, V, before returning them. Because Java doesn't keep track of generics at runtime, the real return types of the getOldValue() and getNewValue() methods after erasure are Object, anyway. The cast to the generic type isn't actually performed here at runtime, so the @SuppressWarnings("unchecked") annotation is needed to prevent the compiler from alerting us to this fact. Whatever code uses these methods will perform the appropriate cast, however, providing equivalence to the property change listener code we had earlier, except without the need to code casts by hand. We've even provided a copy constructor to allow creating generic property change events from standard non-generic property change events.

There's a problem, however: the Automobile class fires a normal PropertyChangeEvent, not a GenericPropertyChangeEvent<V>. We could change the Automobile class to throw a GenericPropertyChangeEvent<V>, which is compatible with PropertyChangeEvent, but then we'd have to cast the event inside our PropertyChangeListener, which defeats the purpose of this exercise. It looks like we'll have to create a custom GenericPropertyChangeListener<V> as well:

/**A Java Beans property change listener retrofitted
    to use generics to cast to proper value type.
@param <V> The type of property value.
@author Garret Wilson
*/
public interface GenericPropertyChangeListener<V>
    extends PropertyChangeListener
{
  public void propertyChange(final GenericPropertyChangeEvent<V>
      genericPropertyChangeEvent);
}

Well, that was certainly easy enough. But more problems arise. First, the java.beans.PropertyChangeSupport class keeps track of PropertyChangeListeners, not GenericPropertyChangeListeners, and therefore will call the PropertyChangeListener.propertyChange() method rather than the generic version. We could scrap PropertyChangeSupport and keep track of the listeners ourselves, but there bigger problem: our Automobile class would no longer be compatible with the standard Java bound property contract, which requires registration of PropertyChangeListeners and firing of property change events to the non-generics-aware version of propertyChange(). Third party tools would not be able to work with our JavaBean.

The trick is to use a special base listener class that is compatible with both PropertyChangeListener and GenericPropertyChangeListener<V> and that converts the PropertyChangeEvent to a GenericPropertyChangeEvent<V> as needed. That class, com.garretwilson.beans.AbstractGenericPropertyChangeListener, is presented below:

/**A Java Beans property change listener
    retrofitted to use generics to cast to proper value type.
@param <V> The type of property value.
@author Garret Wilson
*/
public abstract class AbstractGenericPropertyChangeListener<V>
   implements GenericPropertyChangeListener<V>
{

  /**Called when a bound property is changed.
  This non-generics version calls the generic version,
      creating a new event if necessary.
  No checks are made at compile time to ensure the given event
      actually supports the given generic type.
  @param propertyChangeEvent An event object describing
      the event source, the property that has changed,
      and its old and new values.
  @see GenericPropertyChangeListener#propertyChange
      (GenericPropertyChangeEvent)
  */
  @SuppressWarnings("unchecked")
  public final void propertyChange(final PropertyChangeEvent
      propertyChangeEvent)
  {
    propertyChange((GenericPropertyChangeEvent<V>)
        getGenericPropertyChangeEvent(propertyChangeEvent));
  }

  /**Converts a property change event to a generics-aware
      property value change event.
  @param propertyChangeEvent An event object describing
      the event source, the property that has changed,
      and its old and new values.
  @return A generics-aware property change event,
      either cast from the provided object
      or created from the provided object's values as appropriate.
  */
  @SuppressWarnings("unchecked")
  public static <T> GenericPropertyChangeEvent<T>
      getGenericPropertyChangeEvent(final PropertyChangeEvent
      propertyChangeEvent)
  {
    if(propertyChangeEvent instanceof GenericPropertyChangeEvent)
    {
      return (GenericPropertyChangeEvent<T>)propertyChangeEvent;
    }
    else  //if the event is a normal property change event
    {
      return new GenericPropertyChangeEvent<T>(
          propertyChangeEvent);  //create a copy of the event
    }
  }
}

Fulfilling the contract of PropertyChangeListener, this class overrides propertyChange(PropertyChangeEvent), but then converts the event to a GenericPropertyChangeEvent<T> and passes it to the generic method version, propertyChange(GenericPropertyChangeEvent<T>). The conversion in the utility method <T> GenericPropertyChangeEvent<T> getGenericPropertyChangeEvent(final PropertyChangeEvent propertyChangeEvent) is efficient: if the event is already a GenericPropertyChangeEvent<T>, there's no need to create a new event. Otherwise, the method creates a new GenericPropertyChangeEvent<T> from the plain PropertyChangeEvent using the copy constructor we created above.

To use this efficiency, we'll need to modify the Automobile class to fire GenericPropertyChangeEvents. (The class would work just fine without this, but would require the unnecessary creation of new objects from generics-aware listeners.) It turns out that PropertyChangeSupport.firePropertyChange(String, Object, Object) just creates a PropertyChangeEvent and sends that to PropertyChangeSupport.firePropertyChange(PropertyChangeEvent). The Automobile class can create its own GenericPropertyChangeEvent<Engine> and fire that object when needed:

/**Sets the car's engine.
@param newEngine The new engine.
@exception NullPointerException if the engine
    is <code>null</code>.
*/
public void setEngine(final Engine newEngine)
{
  if(newEngine==null)
  {
    throw new NullPointerException("No engine provided.");
  }
  if(!engine.equals(newEngine))
  {
    final Engine oldEngine= engine;
    engine=newEngine;
    final PropertyChangeEvent propertyChangeEvent=
        new GenericPropertyChangeEvent<Engine>(this,
                                                     "engine",
                                                     oldEngine,
                                                     newEngine);
    propertyChangeSupport.firePropertyChange(propertyChangeEvent);
  }
}

All the pieces are now in place. We can now listen to the car's engine changing using our generics-aware classes with no casts:

final Automobile automobile=new Automobile();
automobile.addPropertyChangeListener("engine",
    new AbstractGenericPropertyChangeEvent<Engine>()
    {
      public void propertyChange(
          final GenericPropertyChangeEvent<Engine>
          propertyChangeEvent)
      {
        final Engine newEngine=propertyChangeEvent.getNewValue();
        if(newEngine.getSize()>8)
        {
          Systen.out.println("That's a big engine.");
        }
      }
    });

Furthermore, non-generics-aware PropertyChangeListener instances can still register with an Automobile instance, and they will function as normal. Even better, we can use the AbstractGenericPropertyChangeEvent<V> registration pattern to register with any JavaBean, whether or not it is generics-aware. Our code will perform casts implicitly, obviating the need for us to perform this tedious and error-prone process ourselves. This pattern additionally imposes consistency on our code—if we copy part of this routine to another property change listener that expects a type other than Engine, our code won't compile, whereas an erroneous hand-coded cast to Engine in a non-generics-aware property change listener would have gone unnoticed until runtime.





Page 1 of 2



Comment and Contribute

 


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

 

 


Sitemap | Contact Us

Rocket Fuel