Replace 1.1 event adapters to build better apps

Sophisticated event adapters can replace JDK 1.1's adapters, improving your applications

T he event adapter classes introduced with JDK 1.1 are simple and lightweight, but they do not provide enough capability for medium or large Java applications. Their shortcomings are legion: too many problems go unreported to the user; GUI responsiveness is missing when actions take a long time to complete; and an application's error-handling policy cannot be changed easily. Given all this, what's a developer to do?

This article demonstrates how to address these issues by using sophisticated event adapter classes designed to improve on those provided with JDK 1.1.

The following applet demonstrates this article's sophisticated adapters in action. The top row of the applet uses the sophisticated adapters, with the result that various errors are caught and the user is notified. The Long Asynch Action button shows the asynchronous behavior in action; notice that the GUI is not hung while it sleeps.

The bottom row uses standard adapters; note that the errors detected by the top components go undetected. The Long AWT-Blocking Action button shows the GUI hung.

You need a JDK 1.1-enabled browser to see this applet.

Note: Not all browsers have complete JDK 1.1 support. If the applet does not appear in a pop-up window in the upper left-hand corner of your browser window, you will need to download the applet. To do so, just click on this handy zip file.

Adapter classes in JDK 1.1 -- benefits and limitations

First, let's look at the simple event adapters you get with the JDK 1.1. They aren't very complicated because each method in each JDK 1.1 adapter does absolutely nothing. There is no ActionAdapter class for the simple ActionListener interface, but if an ActionAdapter class existed, it would look like this:

    public class ActionAdapter implements ActionListener
    {
        public void actionPerformed (ActionEvent e)
        {
        }
    }

The other adapter classes provided by the JDK are larger, but no more complicated. These simple adapters do have good features:

  • They are very lightweight.
  • Their "do nothing" methods are as efficient as possible.
  • They do not enforce a specific policy on programs. Since any policy is likely to be wrong for some programs, this safe decision allows different programs to implement different policies. In fact, this article's adapters might have been more difficult to write had the JDK-provided adapters been more complex than they are.
  • They ship with the Java runtime environment, so applets don't need to download them.

Unfortunately, the JDK 1.1-provided adapters also have four major limitations.

Limitation 1: The JDK 1.1 adapters do not catch RuntimeExceptions

Quick, answer this: Can this method throw any exceptions?

    public void niftyMethod ()   // Note: no throws clause!
    {
        // Do stuff.
    }

In fact, it can. The Java language defines an entire class hierarchy of Exception objects that can be thrown even without a throws XXXException clause. These exceptions descend from a base class named java.lang.RuntimeException. Examples include IndexOutOfBoundsException and NullPointerException.

A RuntimeException usually indicates that there is a bug in the program; it may be trying to use a null reference or indexing off the end of an array. Nevertheless, the standard adapters allow programming errors like this to go undetected by the program and unreported to the user of the program. The practical result is that programs can fail silently, preventing the user from discovering the problem until it's too late.

The following sequence illustrates this scenario:

            User: "Please save my work."
    Java Program: Hmmm. There's a null pointer exception so I can't save
                  the work ... but it isn't caught, so I don't need
                  to tell the user.
                  "OK!"
            User: "Exit."
    Java Program: "Bye!"
            User: "Hey, where's my file?!?"

Well-behaved programs should never fail silently. A better exchange would look something like this:

            User: "Please save my work."
    Java Program: Hmm. There's a null pointer exception so I can't save
                  the work. Ah, I'll report it.
                  "I can't save your work because I got a null pointer
                  exception!"
            User: "What the heck is a null pointer?  Well, just to be
                  safe, I think I'll save this to the clipboard and
                  dump it in a text file ..."

The user might still lose some information (formatting, in this example) but this is better than losing everything.

Limitation 2: The JDK 1.1 adapters do not catch Errors

Next question: Can anything be thrown from the following improved function?

    public void niftyMethodTwo ()
    {
        try
        {
            // Do stuff.
        }
        catch (Exception e)    // Note: We are catching all exceptions!
        {
            // Panic.
        }        
    }

Again, the question has an affirmative answer. The Java language defines an entire category of failures, called Errors, that are not exceptions. Errors usually indicate that something has gone wrong with the underlying virtual machine or with the program itself. Examples include OutOfMemoryError and StackOverflowError.

As with runtime exceptions, errors should not happen during program execution. Again, however, if they do happen, the user should receive notification. The following exchange illustrates the problem resulting from not dealing with Errors:

            User: "Please save my work."
    Java Program: Hmm. I'm out of memory so I can't save the work, 
                  but errors aren't caught so I don't need to tell
                  the user.
                  "OK!"
            User: "Exit."
    Java Program: "Bye!"
            User: "Did I lose my file again?!?"

Again, if users receive notification of the problem, they can at least try to recover their work:

            User: "Please save my work."
    Java Program: Hmm. I'm out of memory so I can't save
                  the work. Ah, I'll report it.
                  "I can't because I'm out of memory!"
            User: "Darn. Close these other files."
    Java Program: "OK."
            User: "Now please save my work."
    Java Program: Hmm, everything worked that time.
                  "OK!"
            User: "That worked well. I'm going to start a fan
                  club for the programmers who wrote this program."

To reiterate, well-behaved programs should not fail silently.

Limitation 3: The JDK adapters run on the AWT thread

All the methods in the default adapters run on the AWT thread. Both the AWT and Swing are single-threaded, so the entire GUI freezes while an action executes. Even the application's other frames will not respond to new GUI events while the application handles a prior event. Indeed, if the action runs for very long, the application can appear to be hung.

A practical example of this would be printing a large document:

            User: "Please print my huge document."
    Java Program: OK, here I go ... I'll format it first.
            User: "I'd like to edit another file." The user clicks
                  the mouse on the File menu. Nothing happens
    Java Program: (to itself) I'm still formatting ... la-de-dah ...
            User: "Hello? Are we locked up?"
    Java Program: (still to itself) I'm still formatting ... tum-tee-tum ...
            User: Starts clicking madly ... nothing happens ...
                  user starts getting upset.    

A better-behaved application would put the printing in the background. The sequence might then go like this:

            User: "Please print my huge document."
    Java Program: OK, here I go ... I'll do this in the background
                  so the user can edit other documents.
            User: "I'd like to edit another file." The user clicks
                  the mouse on the File menu. The File menu drops down
                  and the user selects a new file to edit
    Java Program: (smugly) I'm still formatting, but I can load a new
                  file at the same time because I'm so clever.
            User: The user starts typing.
    Java Program: (getting even more smug) I'm still formatting, but
                  I can edit and print because I'm so clever.

There are still complicated design problems that must be addressed before a program can safely do two things at once, but users don't care. Properly designed programs should never appear to be locked up, a situation the standard adapters do not help to avoid.

Limitation 4: The JDK 1.1 adapters provide no single point for an exception-handling policy

If the standard adapters are used, each handler must provide its own try/catch block and do something with the Exceptions and Errors that are caught. This process is both tedious and mistake prone. Worse, if the policy for handling Exceptions and Errors changes, there will be an enormous amount of cut-and-paste rework. Imagine, for example, that version 1 of an application does not log errors, but that we want to add this feature in version 2. It would be nice to make this change in one place rather than in hundreds. A class hierarchy sporting a single place to implement an error-handling policy makes this sort of change easy.

More complete adapters

Robust applications should use adapters that address all of these concerns.

To review:

  • All thrown Exceptions and Errors should be caught and reported. The application must not fail silently.
  • Adapters should be able to run on separate threads. This keeps the GUI from locking up while handling long-running actions.
  • There should be a single place to implement an error-handling policy. This reduces the amount of code and allows the policy to change easily.

Finally, we want programmers to use our new adapters just like the standard adapters. Indeed, our new adapters should not be more complicated to use than the standard adapters.

AbstractSophisticatedAdapter.java

The first step is to place most of the error handling and reporting into an abstract base class -- AbstractSophisticatedAdapter. All our other adapter classes will inherit from this base class. Let's take a look:

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

/** * This class provides most of the infrastructure and default behavior * for the adapter classes provided. It does _not_ provide the * infrastructure for the asynchronous handling. */ public abstract class AbstractSophisticatedAdapter { // An single object implementing a policy for handling exceptions // and errors for all adapters. // static private ThrowableHandlerIFC throwableHandler = new DefaultThrowableHandler();

/** * This class defines the interface required for installable * Error and Exception handler objects. All Exceptions are * handled by one method. Errors are separated into * those Errors that might be recoverable, and all the rest. * The breakout for Error classes is practical, not theoretical. */ public static interface ThrowableHandlerIFC { public void handleException (Exception e);

public void handleOutOfMemoryError (OutOfMemoryError error);

public void handleExceptionInInitializerError (ExceptionInInitializerError error);

public void handleNoClassDefFoundError (NoClassDefFoundError error);

public void handleUnsatisfiedLinkError (UnsatisfiedLinkError error);

public void handleStackOverflowError (StackOverflowError error);

public void handleOtherError (Error error); }

/** * This class provides an implementation of the ThrowableHandlerIFC * that handles all errors and exceptions by displaying a modal dialog * box. More sophisticated handlers are easily imaginable. */ public static class DefaultThrowableHandler implements ThrowableHandlerIFC { // This is the dialog class. // static class ErrorDialog extends Dialog { // We need a parent Frame object for the Dialog object, // but since we make a modal Dialog object it doesn't // matter what Frame object we use. Since it doesn't matter, // it is easier to just make // our own Frame rather than passing in a Frame as a parameter. // static Frame baseFrame = new Frame();

public ErrorDialog (Throwable throwable) { super (baseFrame, "Ooops! " + throwable.getClass().getName(), true);

setLayout (new BorderLayout (1, 1)); add (new Label (throwable.getLocalizedMessage()), "Center");

Button closeButton = new Button ("Drat!"); closeButton.addActionListener (new CloseAction()); add (closeButton, "South"); setSize (300, 100); }

private class CloseAction implements ActionListener { public void actionPerformed (ActionEvent event) { setVisible (false); dispose(); } } }

public void handleException (Exception e) { new ErrorDialog (e).show(); }

public void handleOutOfMemoryError (OutOfMemoryError error) { new ErrorDialog (error).show(); }

public void handleExceptionInInitializerError (ExceptionInInitializerError error) { new ErrorDialog (error).show(); }

public void handleNoClassDefFoundError (NoClassDefFoundError error) { new ErrorDialog (error).show(); }

public void handleUnsatisfiedLinkError (UnsatisfiedLinkError error) { new ErrorDialog (error).show(); }

public void handleStackOverflowError (StackOverflowError error) { new ErrorDialog (error).show(); }

public void handleOtherError (Error error) { new ErrorDialog (error).show(); } }

/** * This method allows for the installation of a new handler * if the default handler provided is not acceptable. All * adapter objects in an application share the same default * handler. */ static public void installDefaultExceptionHandler (ThrowableHandlerIFCnewHandler) { if (newHandler == null) throw new NullPointerException("Default throwable handler cannot throwableHandler = newHandler; }

/** * Dispatch Exceptions to the installed handler. * This method is protected so that subclasses can override * this behavior if desired. */ protected void handleException (Exception exception) { throwableHandler.handleException (exception); }

protected void handleOutOfMemoryError (OutOfMemoryError error) { throwableHandler.handleOutOfMemoryError (error); }

protected void handleExceptionInInitializerError (ExceptionInInitializerError error) { throwableHandler.handleExceptionInInitializerError (error); }

protected void handleNoClassDefFoundError (NoClassDefFoundError error) { throwableHandler.handleNoClassDefFoundError (error); }

protected void handleUnsatisfiedLinkError (UnsatisfiedLinkError error) { throwableHandler.handleUnsatisfiedLinkError (error); }

protected void handleStackOverflowError (StackOverflowError error) { throwableHandler.handleStackOverflowError (error); }

protected void handleOtherError (Error error) { throwableHandler.handleOtherError (error); }

/** * Concrete implementations can override this method to provide * behavior to execute after any errors and exceptions have * been dealt with. An example of this might be code to * re-enable GUI components that were disabled while the * action was executing. This method is especially useful * for asynchronously executing adapters. */ protected void finallyImpl () { }

/** * This function takes an Error object and calls the appropriate * handleXXXError() method. */ protected void dispatchError (Error error) { if (error instanceof OutOfMemoryError) handleOutOfMemoryError ((OutOfMemoryError)error); else if (error instanceof OutOfMemoryError) handleOutOfMemoryError ((OutOfMemoryError)error); else if (error instanceof ExceptionInInitializerError) handleExceptionInInitializerError ((ExceptionInInitializerError)error); else if (error instanceof NoClassDefFoundError) handleNoClassDefFoundError ((NoClassDefFoundError)error); else if (error instanceof UnsatisfiedLinkError) handleUnsatisfiedLinkError ((UnsatisfiedLinkError)error); else if (error instanceof StackOverflowError) handleStackOverflowError ((StackOverflowError)error); else handleOtherError (error); } }

SophisticatedActionAdapter.java

You can't use classes that don't exist, so, to get the benefits of the sophisticated adapters presented in this article, each JDK-provided listener interface must have a related sophisticated adapter class. Because ActionListener is one of the simplest and most useful listener interfaces, we'll start by presenting the sophisticated adapter for it. The SophisticatedActionAdapter presented below demonstrates how to use AbstractSophisticatedAdapter, outlined above, as a base class, and illustrates adding asynchronous behavior to specific adapter classes.

import java.awt.event.*;

/** * This class should be used in place of java.awt.ActionListener. * It provides robust error and exception handling as well as * the ability to run the action on a thread other than the * AWT thread. */ public abstract class SophisticatedActionAdapter extends AbstractSophisticatedAdapter implements ActionListener { // This is null if actions should be handled on // their own threads and non-null if events // should be handled on the AWT thread. See actionPerformed() // and setAsynchronousHandlingEnabled() to see how this works. // private AsynchThread awtThreadObject;

/** * Construct a new SophisticatedActionAdapter object that * handles ActionEvents on the AWT thread. */ public SophisticatedActionAdapter () { awtThreadObject = new AsynchThread(null); }

/** * Construct a new SophisticatedActionAdapter object that * handles ActionEvents on the AWT thread if 'runAsynchronously' * is false, and on its own thread if 'runAsynchronously' is true. */ public SophisticatedActionAdapter (boolean runAsynchronously) { setAsynchronousHandlingEnabled (runAsynchronously); }

/** * Set whether the ActionEvents are processed on the AWT thread * or asynchronously on their own threads. */ public void setAsynchronousHandlingEnabled (boolean runAsynchronously) { if (runAsynchronously == false) awtThreadObject = new AsynchThread(null); else awtThreadObject = null; }

/** * Specific child classes should override actionPerformedImpl() * instead of this method. This method is final so that * the compiler will detect the common mistake of overriding * this method. */ public final void actionPerformed (ActionEvent event) { if (awtThreadObject != null) awtThreadObject.run(event); else new AsynchThread (event).start(); }

/** * Client code should override this method instead of actionPerformed(). * If the ActionEvent is to be handled on its own thread, the subclass * is responsible for calling SwingUtilities.invokeLater() and * SwingUtilities.invokeAndWait() as needed. */ protected void actionPerformedImpl (ActionEvent event) throws Exception { }

// This class handles the asynchronous event dispatching. // For convenience, all event dispatching -- synchronous // and asynchronous -- goes through an object of this class. // See the run() method in this class and the actionPerformed() // in SophisticatedActionAdapter to see how this works. // private class AsynchThread extends Thread { private ActionEvent event;

public AsynchThread (ActionEvent event) { this.event = event; }

private void run (ActionEvent event) { this.event = event; run(); }

public void run () { try { actionPerformedImpl (event); } catch (Exception exception) { handleException (exception); } catch (Error error) { dispatchError (error); } finally { finallyImpl(); } } } }

SophisticatedKeyAdapter.java

KeyListener is a more complicated listener interface with multiple methods. SophisticatedKeyAdapter is the appropriate sophisticated adapter class for KeyListener. Furthermore, it can serve as a template for other adapters implementing multiple event methods.

SophisticatedKeyAdapter, presented below, incorporates two important differences from SophisticatedActionAdapter. First, multiple event methods funnel though the run() method in the AsynchThread inner class. Second, the asynchronous behavior is turned on or off for the entire object, not for each method in the class.

import java.awt.event.*;

public class SophisticatedKeyAdapter extends AbstractSophisticatedAdapter implements KeyListener { // These values are used to route events properly // through the run() method in the AsynchThread inner class. // static private final int KEY_PRESSED = 0; static private final int KEY_RELEASED = 1; static private final int KEY_TYPED = 2;

private AsynchThread awtThreadObject;

public SophisticatedKeyAdapter () { awtThreadObject = new AsynchThread(null); }

public SophisticatedKeyAdapter (boolean runAsynchronously) { setAsynchronousHandlingEnabled (runAsynchronously); }

public void setAsynchronousHandlingEnabled (boolean runAsynchronously) { if (runAsynchronously == false) awtThreadObject = new AsynchThread(null); else awtThreadObject = null; }

public final void keyPressed (KeyEvent event) { if (awtThreadObject != null) awtThreadObject.run(event, KEY_PRESSED); else new AsynchThread (event).start(); }

public final void keyReleased (KeyEvent event) { if (awtThreadObject != null) awtThreadObject.run(event, KEY_RELEASED); else new AsynchThread (event).start(); }

public final void keyTyped (KeyEvent event) { if (awtThreadObject != null) awtThreadObject.run(event, KEY_TYPED); else new AsynchThread (event).start(); }

/** * Client code should override this method instead of keyPressed(). */ protected void keyPressedImpl (KeyEvent event) throws Exception { }

/** * Client code should override this method instead of keyReleased(). */ protected void keyReleasedImpl (KeyEvent event) throws Exception { }

/** * Client code should override this method instead of keyTyped(). */ protected void keyTypedImpl (KeyEvent event) throws Exception { }

private class AsynchThread extends Thread { private KeyEvent event; private int eventType; public AsynchThread (KeyEvent event) { this.event = event; }

private void run (KeyEvent event, int eventType) { this.event = event; this.eventType = eventType; run(); }

public void run () { try { if (eventType == KEY_PRESSED) keyPressedImpl (event); else if (eventType == KEY_RELEASED) keyReleasedImpl (event); else keyTypedImpl (event); } catch (Exception exception) { handleException (exception); } catch (Error error) { dispatchError (error); } finally { finallyImpl(); } } } }

Obviously, there are more listener classes beyond KeyListener and ActionListener. With this in mind, you should provide each additional listener class with an appropriate SophisticatedXXXAdapter class, as needed.

Implications

The adoption of sophisticated adapter classes could have several potential drawbacks:

  • The GUI runs more slowly: The GUI code actually runs more slowly using the sophisticated adapter classes. This extra time should not matter too much, since adding a few microseconds overhead to a GUI action probably won't be noticed.
  • Small applets download more slowly: The sophisticated adapters add code, which means that applets download more slowly. The good news is that, if an applet needs the sophisticated adapter capability, providing it in a base class should result in less total code to download than writing try/catch blocks inside each adapter. However, if the applet is small and simple enough not to need this capability, these classes may be unnecessary. For small applets, the adapters provided in the JDK 1.1 are often sufficient.
  • Using the asynchronous capability is tricky: Using the asynchronous capability correctly and safely is tricky. Because the JDK 1.1 adapters run their code on the AWT thread, programmers can write GUIs without worrying about multithreaded programming. However, once a program uses the sophisticated adapters' asynchronous capabilities, the specific adapter subclasses must be written with multithreading in mind. While multithreaded programming is often necessary to keep the GUI from appearing locked up, this necessity does not make the multithreaded programming work any easier.
  • Running out of memory is still dangerous: The sophisticated adapters provided above do not always deal well with running out of memory. In fact, they might fail while trying to report that the program is out of memory because there isn't even enough memory to report the problem! Various strategies can be attempted with installable error handling policies to address this, but none solve the problem completely.

Integrating the classes with IDEs

Drag and drop IDE environments like Visual Café and JBuilder don't use this article's sophisticated adapter classes. To use these adapter classes with an IDE, let the IDE generate code using the standard adapters, then edit the generated code. This is annoying but not difficult.

What about JDK 1.0?

The problems solved by these sophisticated adapter classes are still problems for JDK 1.0 applets and applications. Unfortunately, the adapter classes shown above don't work with JDK 1.0 because it has a different event model. Even worse, there are no good solutions using the 1.0 event model.

A possible workaround: SophisticatedButton.java

To ensure that Errors and RuntimeExceptions in JDK 1.0 applets do not go unreported, one can subclass each AWT component and implement the correct try/catch behavior in the subclass. Client code should then use these subclasses instead of the raw AWT classes. An incomplete example for Button is illustrated here:

import java.awt.*;

/** * This button subclass catches all Errors and Exceptions. * Use this class only if you are running in a JDK 1.0 environment. */ public class SophisticatedButton extends Button { public final boolean handleEvent(Event evt) { try { handleEventImpl (evt); } catch (Exception exception) { handleException (exception); } catch (Error error) { dispatchError (error); } finally { finallyImpl(); }

return true; }

/** * Client code overrides this method instead of handleEvent(). */ protected void handleEventImpl (Event evt) { }

protected void handleException (Exception e) { // Dispatch to a global class like // AbstractSophisticatedAdapter, but rewritten // to work with JDK 1.0. }

protected void dispatchError (Error error) { // Dispatch to a global class like // AbstractSophisticatedAdapter, but rewritten // to work with JDK 1.0 (so no inner classes...) }

protected void finallyImpl () { // Dispatch to a global class like // AbstractSophisticatedAdapter, but rewritten // to work with JDK 1.0 (so no inner classes...) } }

To add more functionality, you can provide a Thread subclass for each SophisticatedXXX class, then rewrite the try block inside the handleEvent() method to dispatch to a new Thread object, if appropriate. This code would look similar to the dispatch code in the SophisticatedActionAdapter class above.

This is a lot of work, and it scales poorly due to the cutting-and-pasting necessary for each AWT GUI component. Third party components must be subclassed too, which adds even more work. It will often be simpler to require a JDK 1.1 environment instead of trying to make this work under JDK 1.0.

Conclusion

The standard adapter classes provided by the JDK are simple, but do not provide the functionality required by even moderately sophisticated Java applications; they allow for too many silent failures, provide no help in keeping the GUI responsive, and do not allow an application's exception-handling policy to be easily changed.

In contrast, this article's sophisticated adapter classes, which address the shortcomings of the JDK 1.1's adapters, can help produce more robust and responsive applications. Nevertheless, the presented adapter classes are not the correct solution for all applications. Each application development team must decide whether these classes provide not enough functionality, enough, or too much. Someone still has to think.

Mark is JavaWorld's Java Tip technical coordinator. He has been programming professionally since 1989 and has been using Java since the alpha-3 release. He works full time at KLA-Tencor and is part of a team building a huge, distributed, parallel, multicomputer application for image processing (among other things) that is written almost entirely in Java.

Learn more about this topic