The first advantage of the callback scheme over the polling scheme is that it doesn't waste so many CPU cycles. But a much more important advantage is that callbacks are more flexible and can handle more complicated situations involving many more threads, objects, and classes. For instance, if more than one object is interested in the result of the thread's calculation, the thread can keep a list of objects to call back. Particular objects can register their interest by invoking a method in the Thread or Runnable class to add themselves to the list. If instances of more than one class are interested in the result, a new interface can be defined that all these classes implement. The interface would declare the callback methods. If you're experiencing déjà vu right now, that's probably because you have seen this scheme before. This is exactly how events are handled in Swing, the AWT, and JavaBeans. The AWT runs in a separate thread from the rest of the program; components and beans inform you of events by calling back to methods declared in particular interfaces, such as ActionListener and PropertyChangeListener. Your listener objects register their interests in events fired by particular components using methods in the Component class, such as addActionListener( ) and addPropertyChangeListener( ). Inside the component, the registered listeners are stored in a linked list built out of java.awt.AWTEventMulticaster objects. It's easy to duplicate this pattern in your own classes. Example 5-9 shows one very simple possible interface class called DigestListener that declares the digestCalculated( ) method.
Example 5-9. DigestListener interface
public interface DigestListener { public void digestCalculated(byte[] digest); }
Example 5-10 shows the Runnable class that calculates the digest. Several new methods and fields are added for registering and deregistering listeners. For convenience and simplicity, a java.util.Vector manages the list. The run( ) method no longer directly calls back the object that created it. Instead, it communicates with the private sendDigest( ) method, which sends the digest to all registered listeners. The run( ) method neither knows nor cares who's listening to it. This class no longer knows anything about the user interface class. It has been completely decoupled from the classes that may invoke it. This is one of the strengths of this approach.
Example 5-10. The ListCallbackDigest class
import java.io.*; import java.security.*; import java.util.*; public class ListCallbackDigest implements Runnable { private File input; List listenerList = new Vector( ); public ListCallbackDigest(File input) { this.input = input; } public synchronized void addDigestListener(DigestListener l) { listenerList.add(l); } public synchronized void removeDigestListener(DigestListener l) { listenerList.remove(l); } private synchronized void sendDigest(byte[] digest) { ListIterator iterator = listenerList.listIterator( ); while (iterator.hasNext( )) { DigestListener dl = (DigestListener) iterator.next( ); dl.digestCalculated(digest); } } public void run( ) { try { FileInputStream in = new FileInputStream(input); MessageDigest sha = MessageDigest.getInstance("SHA"); DigestInputStream din = new DigestInputStream(in, sha); int b; while ((b = din.read( )) != -1) ; din.close( ); byte[] digest = sha.digest( ); this.sendDigest(digest); } catch (IOException ex) { System.err.println(ex); } catch (NoSuchAlgorithmException ex) { System.err.println(ex); } } }
Finally, Example 5-11 is a main program that implements the DigestListener interface and exercises the ListCallbackDigest class by calculating digests for all the files named on the command line. However, this is no longer the only possible main program. There are now many more possible ways the digest thread could be used.
Example 5-11. ListCallbackDigestUserInterface interface
import java.io.*; public class ListCallbackDigestUserInterface implements DigestListener { private File input; private byte[] digest; public ListCallbackDigestUserInterface(File input) { this.input = input; } public void calculateDigest( ) { ListCallbackDigest cb = new ListCallbackDigest(input); cb.addDigestListener(this); Thread t = new Thread(cb); t.start( ); } public void digestCalculated(byte[] digest) { this.digest = digest; System.out.println(this); } public String toString( ) { String result = input.getName( ) + ": "; if (digest != null) { for (int i = 0; i < digest.length; i++) { result += digest[i] + " "; } } else { result += "digest not available"; } return result; } public static void main(String[] args) { for (int i = 0; i < args.length; i++) { // Calculate the digest File f = new File(args[i]); ListCallbackDigestUserInterface d = new ListCallbackDigestUserInterface(f); d.calculateDigest( ); } } }