Generating Event Listeners
Dynamically
Swing
uses ActionListeners to add behavior to nearly all of its GUI
controls -- and the anonymous classes that JavaBeans use to implement
event listeners have been accused in some quarters of consuming
more class file space than they should. This article shows how
Swing can help you get around that problem. With the help of several
code samples that you can download and experiment with, it offers
some suggestions that may be of interest to developers of IDEs
and other kinds of applications that generate listeners for Swing
or AWT.
By Hans Muller
One charge that gets leveled against the Java Beans event model
from time to time is that the anonymous classes used to implement
event listeners consume more class file space than they should.
Event listener classes implement one of the many interfaces derived
from java.util.EventListener . For example, to handle
keyboard events, you can add an implementation of java.awt.event.KeyListener
to a component's "key" listener list:
KeyListener myKeyListener = new KeyListener() {
public void keyTyped(KeyEvent e) { handleKeyEvent(e); }
public void keyPressed(KeyEvent e) { handleKeyEvent(e); }
public void keyReleased(KeyEvent e) { handleKeyEvent(e); }
};
myComponent.addKeyListener(myKeyListener);
As you can see, the listener in this case simply dispatches the
KeyEvent to another method, called handleKeyEvent() .
Using a listener as a sort of "trampoline" to shuttle events from
a listener method to some event-handling method in the outer class
is a common idiom. In fact, some GUI builders generate boilerplate
listener implementations that do exactly that. Today's JavaTM
compilers produce a class file like "myListener"
for each anonymous inner class. Usually the class file gets a funny
name such as "MyClass$3".
The ActionListener Problem
Fortunately, most applications don't have many KeyListeners. However,
Swing applications tend to be teeming with ActionListeners. ActionListeners
are used to add behavior to nearly all of Swing's GUI controls --
notably buttons, combo boxes, and menu items.
This can be a serious concern in applets; applets should download
as quickly as possible, and yet each inner class file used in an
applet contains a block of potentially redundant information (beyond
the few byte codes that represent the applet's listener method implementations
themselves). If these listener classes aren't just trampolines but
actually do significant work, then the cost of the extra class files
probably isn't worth worrying about. However, when an IDE or other
system unconditionally generates this kind of thing, it's worth
considering the remedies presented in this article.
How the Remedies Work
To illustrate how these remedies work, this article presents several
source files, titled:
You'll get a chance to see how all these parts fit together later
in the article.
Using Reflection
to Implement a Generic ActionListener
In the future, it's quite possible that separate class files will
not be generated for each inner class, or that inner class files
will be relatively small. However, in the short term, the problem
does exist, so the solutions outlined in this article should be
of interest to anyone developing an IDE or other application that
generates listeners for Swing or AWT.
Many developers have observed that by using reflection to dispatch
from a listener to some other handling method, one can get by with
just one listener implementation per listener type. The following
example (with exception handling omitted) shows how easy it is to
create a generic ActionListener that invokes a method on some target
object :
class GenericActionListener implements ActionListener {
private final Object target;
private final Method targetMethod;
GenericActionListener(Object target, Method targetMethod) {
this.target = target;
this.targetMethod = targetMethod;
}
public void actionPerformed(ActionEvent e) {
targetMethod.invoke(target, new Object[]{e});
}
}
The one class created in this example -- GenericActionListener
-- can be used for all of the ActionListeners in an application.
To use GenericActionListener, you must look up the method object
that the listener will invoke. For example, to create an ActionListener
that calls an ActionEvent method called buttonAction ,
you could write a method like this (exception handling ommitted):
Method m = getClass().getMethod("myAction", new Class[]{ActionEvent.class});
myButton.addActionListener(new GenericActionListener(this, m));
This kind of solution works well enough for small GUI applications.
In general, however, it requires the creation of a similar boilerplate
class for each listener type, and that can get oppressive. As it
turns out, it's possible to generate the bytecodes that define simple
classes like GenericActionListener at runtime -- and they can be
generated for any listener interface. The fact that such a class
can be generated at runtime is of particular importance to developers
of IDEs and other tools because it enables event links to be created
between components (classes) that are loaded at run-time -- that
is, those whose listener interfaces aren't known until runtime.
GenericListener: Dynamically Generating
Listener Classes
So far, we've defined a new utility class called GenericActionListener
that can be used to generate a listener interface implemention in
which one listener method dispatches to a method on some target
object. To see how this new class works, you can download and examine
the source files supplied with this article:
The GenericListener.create() method code generates
a "trampoline" listener class, if one doesn't exist already, and
then loads the new class with a custom class loader. The trampoline
class is configured to apply a method that you specify to a target
object. If you take the black-box view of GenericListener.create() ,
then you provide a listener method, a target object, and a target
object method, and the create() method returns a listener
object where the specified listener method calls targetObject.targetMethod(event) .
Here's an example (We're using the a convenient version of the
create() method that allows us to specify string method
names):
ActionListener l = (ActionListener)(GenericListener.create(
ActionListener.class,
"actionPerformed",
myTargetObject,
"myTargetObjectMethod"));
myButton.addActionListener(l);
Here we've created an ActionListener whose actionPerformed()
method calls myTargetObject.myTargetObjectMethod(anActionEvent) .
If the ActionListener interface doesn't contain a method named actionPerformed(),
or if the target object doesn't contain a method named myTargetObjectMethod()
that matches the signature of actionPerformed(),
a runtime exception is thrown. Naturally, the value returned by
the static GenericListener.create() method has to be
cast to the correct listener type, since it can return any type
of listener.
To see a complete program that creates a MouseListener and an
ActionListener, see the Demo.java
example.
This approach has two important advantages of this approach over
the strategy of managing a set of trampoline classes that was described
in the preceding section:
- The internal listener "trampoline" classes are generated automatically
and at runtime. They don't affect download time, and they have
no more effect on the applications working set than a minimal
set of similar hand-written classes would.
- IDEs and other tools that load classes or components dynamically
can use GenericListener to generate event bindings for listener
interfaces (and event types) that aren't known until run-time.
WARNING:
The GenericListener implementation takes about 2,000 lines of pure
Java programming language code, and most developers will probably
find it moderately difficult to read -- although developers who
have spent quality time with Java compiler implementations, and
who are familiar with the VM (virtual machine) and the class loader,
shouldn't have too much trouble understanding it.
A variety of simple extensions can be applied to the GenericListener
class itself; for example, it would be easy to add support for having
all listener methods dispatch to the target method. However, our
advice for the rest of the classes is: If you don't like Java compilers,
don't look.
Acknowledgement
The GenericListener class is really just a thin veneer over a
more general-purpose infrastructure for generating arbitrary interface
implementations built by John Rose. John is a member of the Sun/Java
Software Java compiler team. Without his help, we wouldn't have
dared take this work farther than GenericActionListener.
Hans Muller
|