User interfaces for object-oriented systems, Part 5: Useful stuff

Build an application that puts user-interface principles into practice

No theory is worth anything if it can't be applied effectively, so this month and next, I'll build a small but nontrivial application that demonstrates the object-oriented user-interface principles I've covered over the last few months: an RPN (Reverse Polish Notation) calculator that can work as either a tape calculator or a keypad calculator. (One of the main things I want to demonstrate is the flexibility of the UI; thus the two presentations.) Since this application is a real one, I need to do some groundwork before covering the calculator itself.

Consequently, this month's Java Toolbox takes the name of the column literally and presents the set of generic tools we'll need to build the calculator -- little tools of general utility that should be of use for more than the current application. Specifically, I'll cover the following:

Assert An implementation of an "assertion" that lets you implement pre- and postconditions.
Tester A class that aids in putting together automated unit tests.
Bit_bucket The Java equivalent of /dev/null, it throws away characters.
Std A "singleton" wrapper around standard input and output that makes it easy to access these two streams.
Log A convenient class for logging diagnostic messages.
Align A utility containing various methods for aligning text.
Scrollable_text_area A JTextArea that has scroll bars around it.

Most of those classes are unrelated to one another, and none of them is significant enough to merit a article devoted exclusively to it, so lumping them all together in one place seems sensible. We've taken a quick look at a few of the tools in the past, but a review should prove helpful. Now, on to the tools.

TEXTBOX: TEXTBOX_HEAD: Build user interfaces for object-oriented systems: Read the whole series!

:END_TEXTBOX

Assertions

Bertrand Meyer, the author of the Eiffel language, came up with the notion of pre- and postconditions, commonly called assertions. Eiffel, described in Object-Oriented Software Construction (see Resources) supports those concepts directly within the syntax of the language. Java, unfortunately, provides no such support.

The basic idea of a precondition or postcondition is that every method expects the world (the object that it's working on, the global state of the program, the arguments to the method, and so on) to be in a particular state when the method is called, and that every method will leave the world in a particular state. An assertion is a body of code that guarantees that those conditions hold and typically terminates the program with an exception toss if they do not. Assertions are typically implemented as debug-time tests and removed from the code entirely when the code goes into production. (The removal can be risky since you are modifying tested code, but on the other hand, the assertions can slow down the execution of a program noticeably.)

I've implemented assertions using the two classes defined in Lists 1 and 2. The first version, in the com.holub.tools package, is made up of empty methods. Import this version into your code when you've finished debugging. The second version, in the com.holub.tools.debug package (List 2), actually does something. Import this version when you're still working on the code.

When you use the class like this:

Assert.is_true( condition );

the program terminates by throwing an Assert.Failed object if the condition is not true. A second version lets you add a reasonable error message to the thrown object:

Assert.is_true( condition, "Error that caused the problem" );

I've also provided is_false() variants as a programming convenience.

The main problem with my strategy of using an empty method to eliminate the overhead of the assertion is that the resulting efficiency is somewhat JVM dependent. Hotspot, for example, does everything right. It notices that the method is empty, so expands it inline, effectively removing the call from the .class file. Hotspot even removes the argument to the assertion (effectively dead code). The last behavior can be important if you use string concatenation in an argument to is_true() or its equivalent. You don't want to go through the considerable overhead of building a string with a series of concatenations if the string isn't used for anything. Earlier JVMs were not as good about this optimization: they would correctly eliminate the call itself but typically wouldn't eliminate the argument evaluation.

Finally, note that method calls in assertions are risky. Hotspot will evidently inline them up to a point, but it won't chase down the call graph indefinitely. Generally, method calls should appear in assertions only if they do nothing other than return constant values. If you want to test a method's return value, call the method first, then test the resulting value in a subsequent assertion.

Of course, methods that have side effects, which modify the state of the program, should never be invoked from within an assertion because they'll go away when you move from the debugging to the production version of the code.

Let's look at List 1:

Next, we take a look at List 2:

List 2. /src/com/holub/tools/debug/Assert.java
   1: package com.holub.tools.debug;
   2: 
   3: public class Assert
   4: {
   5:   public final static void is_true( boolean expression )
   6:     {   if( !expression )
   7:             throw( new Assert.Failed() );   
   8:     }
   9: 
  10:   public final static void is_false( boolean expression )
  11:     {   if( expression )
  12:             throw( new Assert.Failed() );   
  13:     }
  14: 
  15:   public final static void is_true( boolean expression, String message)
  16:     {   if( !expression )
  17:             throw( new Assert.Failed(message) );    
  18:     }
  19: 
  20:   public final static void is_false( boolean expression, String message)
  21:     {   if( expression )
  22:             throw( new Assert.Failed(message) );    
  23:     }
  24: 
  25:   static public class Failed extends RuntimeException
  26:   {   public Failed(            ){ super("Assert Failed"); }
  27:       public Failed( String msg ){ super(msg);             }
  28:     }
  29: }   
         

Help in unit testing

The next helper class of interest is the Tester, which makes it easier to do unit testing (the standalone testing of a single class).

Test shows a typical use of this class. I typically put my unit tests in a Test inner class of the class I'm testing. (I use an inner class so that the code that makes up the test won't be part of the class file that represents the class I'm testing. Inner classes create their own class files, which in this case do not have to be shipped with the production code.)

I have several criteria for what constitutes proper test behavior. A formal test must:

  1. Not print anything unless something is wrong
  2. Have a unique ID
  3. Test reasonable boundary conditions and "impossible" inputs

The first item is particularly important because, in an automated testing environment, you don't want to clutter up reports with worthless information. A test typically tries to find out when something goes wrong, so that's the only time that output should be generated. On the other hand, I occasionally do get paranoid, so I've provided a hook that modifies the behavior of my class to print both success and failure messages.

The tester is initialized with a normal constructor call that has passed two arguments: first, a Boolean flag that indicates whether the output is verbose (success messages are printed in addition to failures) or not (only failure messages are printed); second, a PrintWriter that represents the stream to which output is sent. Thereafter, you run a test by calling the check() method, which has several overloads.

The arguments are:

  • The test ID
  • The expected output of a method call
  • The method call to test

The overloads change the types of the second and third arguments to handle various possible return values. Supported return values are String, StringBuffer, double (which will handle float via the normal type conversion), long (which will also handle char, int, and so on, via the normal type conversions), and boolean. There is only one requirement: the second argument's type must be the same as the return type of the method call in the third argument.

The final method of interest in the Tester is the exit() method, which causes the program to exist with the current error count as its exit status. This mechanism lets you write shell scripts that run tests and detect when something goes wrong.

List 3. Using the Tester class
   1: private static class Test
   2: {   
   3:   public static void main(String[] args)
   4:     {
   5:     com.holub.tools.Tester t = 
   6:             new com.holub.tools.Tester( args.length > 0,
   7:                                 com.holub.io.Std.out());
   8:     //...
   9:     t.check("align.1", "01234", Align.left("01234", 5));
  10:     t.check("align.2", "     ", Align.left("",      5));
  11:     t.check("align.3", "X    ", Align.left("X",     5));
  12:     //...
  13: 
  14:     t.exit(); // exits with a status equal to the error count
  15:     }
  16: }
         

The code, which is largely self-explanatory, is in List 4:

List 4. /src/com/holub/tools/Tester.java
   1: package com.holub.tools;
   2: 
   3: import com.holub.tools.debug.Assert;
   4: import java.io.*;
   5: 
   6: /****************************************************************
   7:  * A simple class to help in testing. Various check() methods
   8:  * are passed a test id, an expected value, and an actual value.
   9:  * The test prints appropriate messages and keeps track of the
  10:  * total error count for you.
  11:  * For example:
  12:  * 
  13:  * class Test
  14:  * {    public static void main(String[] args);
  15:  *      {
  16:  *          // Create a tester that sends output to standard error, which
  17:  *          // operates in verbose mode if there are any command-line
  18:  *          // arguments.
  19:  *
  20:  *          Tester t = new Tester( args.length > 0,
  21:  *                                      com.holub.tools.Std.out() );
  22:  *          //...
  23:  *
  24:  *          t.check("test.1", 0,     foo());        // check that foo() returns 0.
  25:  *          t.check("test.2", "abc", bar());        // check that bar() returns "abc".
  26:  *          t.check("test.3", true , cow());        // check that cow() returns true
  27:  *          t.check("test.4", true , dog()==null);  // check that dog() returns null
  28:  *
  29:  *          //...
  30:  *          System.exit( t.total_errors() );
  31:  *      }
  32:  *  }
  33:  * 
  34:  */
  35: 
  36: public class Tester
  37: {
  38:   private int               errors = 0;
  39:   private final boolean     verbose;
  40:   private final PrintWriter log;
  41: 
  42:     /**
  43:      * Create a tester that has the specified behavior and output stream:
  44:      *
  45:      * @param verbose   Print messages even if test succeeds. (Normally,
  46:      *                  only failures are indicated.)
  47:      * @param log       If not null, all output is sent here; otherwise
  48:      *                  output is sent to standard error.
  49:      */
  50:   public Tester( boolean verbose, PrintWriter log )
  51:     {   this.verbose = verbose;
  52:         this.log     = log;
  53:     }
  54:     /******************************************************************
  55:      * Check that the expected result of type String is equal to the
  56:      * actual result.
  57:      *
  58:      * @param test_id   String that uniquely identifies this test.
  59:      * @param expected  The expected result
  60:      * @param actual    The value returned from the function under test
  61:      * @return true if the expected and actual parameters matched.
  62:      */
  63: 
  64:   public boolean check( String test_id, String expected, String actual)
  65:     {
  66:         Assert.is_true( log != null    , "Tester.check(): log is null"      );
  67:         Assert.is_true( test_id != null, "Tester.check(): test_id is null"  );
  68: 
  69:         boolean okay = expected.equals( actual );
  70: 
  71:         if( !okay || verbose )
  72:         {   
  73:             log.println (  (okay ? "   okay " : "** FAIL ")
  74:                          + "("  + test_id + ")"
  75:                          + " expected: " + expected
  76:                          + " got     : " + actual
  77:                     );
  78:         }
  79:         return okay;
  80:     }
  81:     /******************************************************************
  82:      * Convenience method, compares a string against a StringBuffer.
  83:      */
  84:   public boolean check( String test_id, String expected, StringBuffer actual)
  85:     {   return check( test_id, expected, actual.toString());
  86:     }
  87:     /******************************************************************
  88:      * Convenience method, compares two doubles.
  89:      */
  90:   public boolean check( String test_id, double expected, double actual)
  91:     {   return check( test_id, "" + expected, "" + actual );
  92:     }
  93:     /******************************************************************
  94:      * Convenience method, compares two longs.
  95:      */
  96:   public boolean check( String test_id, long expected, long actual)
  97:     {   return check( test_id, "" + expected, "" + actual );
  98:     }
  99:     /******************************************************************
 100:      * Convenience method, compares two booleans.
 101:      */
 102:   public boolean check( String test_id, boolean expected, boolean actual)
 103:     {   return check( test_id, expected?"true":"false", actual?"true":"false" );
 104:     }
 105:     /******************************************************************
 106:      * Exit the program, using the total error count as the exit status.
 107:      */
 108:   public void exit()
 109:     {   System.exit( errors );
 110:     }
 111: }
         

The Bit Bucket

Moving on to I/O-related methods, we find that it is sometimes useful to send output to nowhere. Consider a method that takes as its argument a stream to which diagnostic messages are to be sent:

PrintWriter diagnostic_stream = new PrintWriter(System.err, true);
//...
do_something( diagnostic_stream );

Now let's also assume that you don't happen to want these diagnostics to be displayed at all in some context. You could pass in a null argument instead of an output stream, but you might have to test for null in several places in the method, and many methods might have to implement identical tests. A general solution seems in order, and I've provided one with the Bit_bucket class, which is a PrintWriter that simply discards any requested output. You can discard diagnostic output in the previous example like this:

do_something( new Bit_bucket() );

The implementation of Bit_bucket is in List 5. As you can see, it's pretty trivial: it just extends PrintWriter, overriding every method with an empty one.

Singletons and standard input and output

I discussed the Singleton pattern in the context of threads in the April Java Toolbox. A singleton is an object, only one instance of which will ever exist. Don't confuse a singleton with a similar entity: the Booch Utility, a class made up solely of static methods. The Assert class we just looked at is a utility, not a singleton.

Singletons are real objects manufactured at runtime using new. The creation might have to be deferred until runtime for various reasons. For instance:

  1. The class might be quite large and you might not want to load it unless you need it

  2. The class file might not exist in some environments (an NT version of a class might not exist on a Solaris box, for example)

  3. You might not have the information that you need to instantiate the object until runtime

Java's Toolkit class exemplifies all those possibilities.

Since you want to assure that only one instance of a singleton exists, you never create a singleton by calling new. Rather, you call a static method of the singleton that brings the one and only instance into existence the first time the method is called. For example, the first time it is called, Toolkit.getDefaultToolkit() creates (and returns) the actual Toolkit object, an instance of a derived class. Toolkit is an abstract class, so it can't be instantiated. The second call to getDefaultToolkit() just returns the instance created by the first call.

A Singleton class always has nothing but private constructors to assure that nobody can create an instance directly by using new.

The general (much simplified) form of the Singleton class is:

class Singleton
{   static Singleton the_instance;
   private Singleton(){}
    Singleton getInstance()
    {   if( the_instance == null )
            the_instance = new Singleton();
        return the_instance;
    }

This simple structure works only in single-threaded environments. Refer to the April Java Toolbox for a discussion of Singleton in a multithreaded situation. The April column also discusses the JDK_11_unloading_bug_fix class, used below. (This class fixes a bug in the 1.1 JVM, which was too aggressive about garbage-collecting classes.)

The singleton of current interest is the Std class (List 6, line 44), an evolved form of the April version. The main problem I'm solving is the need to continually wrap System.in, System.out, and System.err in the appropriate Reader or Writer object to use them safely in a Unicode world. For example, you should really use:

new PrintWriter(System.out, true).println("Hello World");

rather than:

System.out.println("Hello World")

Creating a PrintWriter every time you want to write to the console is a waste of both system resources and your own time, however. (As an aside, the real problem here is that in, out, and err are public fields. They shouldn't be. Had Sun made them methods that returned an object that implemented a well-defined interface, the implementation of the interface could have been changed without affecting your code at all.)

The Std class provides a solution by creating singletons that represent the wrapped stream. For example, the earlier code can be restated as:

Std.out().println("Hello World");

Each time that the out() method is called, it returns the PrintWriter that it created the first time out() was called.

The following methods are supported:

Std.in() Returns a BufferedReader that wraps a InputStreamReader that wraps System.in.
Std.out() Returns a PrintWriter that wraps a System.out.
Std.err() Returns a PrintWriter that wraps a System.err.
Std.bit_bucket() Returns a PrintWriter (a Bit_bucket, actually) that discards all output.

Each creates the indicated object the first time it's called and returns the same object on subsequent calls. The implementation is in

List 6

.

List 6. /src/com/holub/io/Std.java
   1: package com.holub.io;
   2: import java.io.*;
   3: import com.holub.asynch.JDK_11_unloading_bug_fix;
   4: import com.holub.io.Bit_bucket;
   5: 
   6: /** Convenience wrappers that takes care of the complexity of creating
   7:  *  Readers and Writers simply to access standard input and output.
   8:  *  For example, a call to
   9:  *  
  10:  *  Std.out().println("hello world");
  11:  *  
  12:  *  is identical in function to:
  13:  *  
  14:  *  new PrintWriter(System.out, true).println("hello world");
  15:  *  
  16:  *  and
  17:  *  
  18:  *  String line = Std.in().readLine();
  19:  *  
  20:  *  is identical in function to:
  21:  *  
  22:  *  String line;
  23:  *  try
  24:  *  {   line = new BufferedReader(new InputStreamReader(System.in)).readLine();
  25:  *  }
  26:  *  catch( Exception e )
  27:  *  {   throw new Error( e.getMessage() );
  28:  *  }
  29:  *  
  30:  *  Equivalent methods provide access to standard error
  31:  *  and a "bit bucket" that just absorbs output without printing it.
  32:  *
  33:  *  <p>All these methods create "singleton" objects. For example, the
  34:  *  same PrintWriter object that is created the first time
  35:  *  you call Std.out() is returned by all subsequent calls.
  36:  *  This way you don't incur the overhead of a new with
  37:  *  each I/O request.
  38:  *
  39:  *  @see com.holub.tools.P
  40:  *  @see com.holub.tools.R
  41:  *  @see com.holub.tools.E
  42:  */
  43: 
  44: public final class Std
  45: {
  46:   static{ new JDK_11_unloading_bug_fix(Std.class); }
  47: 
  48:   private static BufferedReader input;        //= null
  49:   private static PrintWriter    output;       //= null
  50:   private static PrintWriter    error;        //= null
  51:   private static PrintWriter    bit_bucket;   //= null
  52: 
  53:     /*******************************************************************
  54:      * A private constructor, prevents anyone from manufacturing an
  55:      * instance.
  56:      */
  57:   private Std(){}
  58: 
  59:     /*******************************************************************
  60:      * Get a BufferedReader that wraps System.in.
  61:      */
  62:   public static BufferedReader in()
  63:     {   if( input == null )
  64:             synchronized( Std.class )
  65:             {   if( input == null )
  66:                     try
  67:                     {   input = new BufferedReader(
  68:                                         new InputStreamReader(System.in));
  69:                     }
  70:                     catch( Exception e )
  71:                     {   throw new Error( e.getMessage() );
  72:                     }
  73:             }
  74:         return input;
  75:     }
  76: 
  77:     /*******************************************************************
  78:      * Get a PrintWriter that wraps System.out.
  79:      */
  80:   public static PrintWriter out()
  81:     {   if( output == null )
  82:             synchronized( Std.class )
  83:             {   if( output == null )
  84:                     output = new PrintWriter( System.out, true );
  85:             }
  86:         return output;
  87:     }
  88: 
  89:     /*******************************************************************
  90:      * Get a PrintWriter that wraps System.err.
  91:      */
  92: 
  93:   public static PrintWriter err()
  94:     {   if( error == null )
  95:             synchronized( Std.class )
  96:             {   if( error == null )
  97:                     error = new PrintWriter( System.err, true );
  98:             }
  99:         return error;
 100:     }
 101: 
 102:     /*******************************************************************
 103:      * Get an output stream that just discards the characters that are
 104:      * sent to it. This convenience class makes it easy to write methods
 105:      * that are passed a "Writer" to which error messages or status information
 106:      * is logged. You could log output to standard output like this:
 107:      *  
 108:      *  x.method( Std.out() );  // pass in the stream to which messages are logged
 109:      *  
 110:      *  but you could cause the logged messages to simply disappear
 111:      *  by calling:
 112:      *  
 113:      *  x.method( Std.bit_bucket() );   // discard normal logging messages
 114:      *  
 115:      */
 116: 
 117:   public static PrintWriter bit_bucket()
 118:     {   if( bit_bucket == null )
 119:             synchronized( Std.class )
 120:             {   if( bit_bucket == null )
 121:                     bit_bucket = new Bit_bucket();
 122:             }
 123:         return bit_bucket;
 124:     }
 125: 
 126:     /** A small test class, reads a line from standard input and
 127:      *  echoes it to standard output and standard error. Run it
 128:      *  with: java com.holub.tools.Std\$Test
 129:      *  (Don't type in the \ when using a Microsoft-style shell).
 130:      */
 131: 
 132:   static public class Test
 133:     {
 134:       static public void main( String[] args ) throws IOException
 135:         {   String s;
 136:             while( (s = Std.in().readLine()) != null )
 137:             {   Std.out().println( s );
 138:                 Std.err().println( s );
 139:                 Std.bit_bucket().println( s );
 140:             }
 141:         }
 142:     }
 143: }
         

Logging diagnostics

The next problem is the logging of diagnostic messages. The Log class differs from a normal output stream in a couple of useful ways. First, it encapsulates (not extends) the Writer that's used to create output. I've done this because it's such a nuisance to catch the IOException that's thrown from the Writer's write method. The Log's version of write() throws a Log.Failure on error, but this class is a RuntimeException, so it doesn't have to be caught explicitly. It's also handy to be able to extract the underlying PrintWriter.

For example, you might want to pass it to an object of the Tester class, discussed earlier. With that in mind, I've provided an accessor method. (I had to jump through hoops to make the accessor safe to use from an object-oriented perspective. I'll say more on this in a moment.)

You use the class like this:

Log log = new Log( "log.test", Std.err() );
log.write( "hello world\n" );
log.write( "xxxhello world\n", 3, 12 );
log.timestamp(); // Turn on per-write-call time stamping.
log.write( "timestamp now on\n" );
log.write( "xxxhello world\n", 3, 12 );
try
{   
    Writer writer = log.writer();   // Get encapsulated Writer.
    writer.write("output directly to writer\n");
    writer.write(new char[]{'c','h','a','r',' ','o','u','t','\n'});
}
catch( IOException exception )      // Must catch exceptions when
{   exception.printStackTrace();    // talking directly to underlying
}                                   // writer.

The arguments to the Log constructor specify the name of the file to which you're logging and a stream to which error messages are sent (which can be Std.bit_bucket()) if you encounter any problems when opening that file. A second constructor (not shown) lets you pass in a Writer rather than a file name so that you can send logging messages to a previously opened stream.

The foregoing code creates the following log file:

#----Wed Dec 01 13:20:04 PST 1999----#
hello world
hello world
Wed Dec 01 13:20:04 PST 1999: timestamp now on
Wed Dec 01 13:20:04 PST 1999: hello world
Wed Dec 01 13:20:04 PST 1999: output directly to writer
char out

Note that the per-line timestamp is prefixed only to those calls that take string arguments.

The implementation of Log is in List 7. There are two main issues here:

  1. Thread safety (I expect a log to be accessed by multiple threads)
  2. Making sure that access to the underlying Writer object is both safe from an object-oriented point of view and thread safe with respect to the Log that contains the Writer

The log-level thread-safety issue is straightforward: just synchronize the methods of the Log class. The main problem is the Writer returned by the writer() method. For both practical and theoretical reasons, direct access to the encapsulated Writer should be impossible. Consequently, the writer() method returns an instance of the Writer_accessor inner class (List 7, line 188), which implements Writer with three twists: First, all the methods except close() just chain through to methods of the encapsulated Writer. The close() method (List 7, line 219) just throws an exception. You must close the Log that contains the Writer.

The second twist is that all the methods explicitly synchronize on the outer-class object. That way, one thread can safely write directly to the Log while another writes via the encapsulated Writer.

Third, a flush() is issued after every output operation in an attempt to make the log as up-to-date as possible if you're logging messages during a catastrophic failure.

List 7. /src/com/holub/io/Log.java
   1: package com.holub.io;
   2: 
   3: import java.io.*;
   4: import java.util.*;
   5: import com.holub.io.Std;
   6: import com.holub.tools.debug.Assert;
   7: // Import com.holub.tools.Assert;
   8: 
   9: /** Encapsulates I/O to a log file. Contains simple wrappers to do
  10:  *  line-level buffering and suck up exceptions that are not errors
  11:  *  in the current context. The Log class encapsulates a Writer
  12:  *  rather than extending it in order to provide more reasonable
  13:  *  exception handling. A Log.Failure object (which is a
  14:  *  RuntimeException) is thrown when something goes wrong
  15:  *  rather than an IOException. This way you don't have to
  16:  *  litter up your code with try blocks. The {@link #writer} method is
  17:  *  provided to allow access to the underlying Writer should
  18:  *  you need it, however.
  19:  *  <p>
  20:  *  This class is thread safe.
  21:  **/
  22: 
  23: public class Log
  24: {
  25:   private Writer                  log_file        = null;
  26:   private static /*final*/ String log_file_name   = "log";
  27:   private boolean                 timestamp       = false; 
  28: 
  29:   public class Failure extends RuntimeException
  30:   {   public Failure(String s){super(s);}
  31:     }
  32: 
  33:     /*******************************************************************
  34:      * Create a new Log with the specified name. If a file with that
  35:      * name exists, then new lines are just appended to it. The current
  36:      * time and date are written into the file as well.
  37:      * <p>
  38:      * An error message is printed if any exceptions are thrown from
  39:      * the I/O system, but the program is permitted to run. (In this
  40:      * case, any subsequent attempts to write to the log are silently
  41:      * ignored.)
  42:      *
  43:      * @param name file to which log messages are sent. If null,
  44:      *              log messages go to standard error.
  45:      * @param error_message_stream. Any error messages that indicate
  46:      *              problems opening the file are sent here.
  47:      *              Use Std.bit_bucket() to suppress error messages.
  48:      */
  49: 
  50:   public Log( String name, PrintWriter error_message_stream )
  51:     {   Assert.is_true( name != null );
  52:         Assert.is_true( error_message_stream != null );
  53: 
  54:         log_file_name = name;
  55:         try
  56:         {   
  57:             error_message_stream.println("Logging to " + name);
  58: 
  59:             log_file = ( name == null )
  60:                     ? (Writer)( Std.err() )
  61:                     : (Writer)( new BufferedWriter(
  62:                             new FileWriter(log_file_name, true /*append*/)) )
  63:                     ;
  64: 
  65:             write( "\n#----" + new Date().toString() + "----#\n" );
  66:         }
  67:         catch( IOException e )
  68:         {
  69:             error_message_stream.println("Couldn't open log file: " 
  70:                                + log_file_name
  71:                                + "(" + e.getMessage() + ")" );
  72: 
  73:             error_message_stream.println("Input will not be logged\n");
  74:         }
  75:     }
  76: 
  77:     /*******************************************************************
  78:      * Writes to a previously opened log file.
  79:      */
  80: 
  81:   public Log( Writer log_file )
  82:     {   Assert.is_true( log_file != null );
  83:         this.log_file      = log_file;
  84:         this.log_file_name = "???";
  85:         write( "\n#----" + new Date().toString() + "----#\n" );
  86:     }
  87: 
  88:     /*******************************************************************
  89:      * Convenience method. Creates a log called "log" in the directory
  90:      * specified by the log.file system property. If the log.file
  91:      * System property (which must be specified with the -D command-line
  92:      * switch) can't be found or if the file can't be opened, then log output
  93:      * is silently discarded.
  94:      */
  95: 
  96:   public Log()
  97:     {   this( System.getProperty("log.file"), Std.err() );
  98:     }
  99: 
 100:     /*******************************************************************
 101:      * All messages logged after this call is made will be prefixed by
 102:      * the current date and time. (This stamp is in addition to the one
 103:      * that's output at the top of the file, which effectively indicates
 104:      * when the log file was opened.)
 105:      */
 106: 
 107:   public void timestamp()
 108:     {   timestamp = true;
 109:     }
 110: 
 111:     /*******************************************************************
 112:      * Append a string of the indicated length, starting at the indicated
 113:      * offset, to the log file. All internal buffers are flushed
 114:      * after writing so that the log file itself will be up-to-date.
 115:      * An error message is printed if any exceptions are thrown from
 116:      * the I/O system.
 117:      * <p>
 118:      * A Failure is thrown if any exceptions are thrown from any internal
 119:      * Writer calls.
 120:      */
 121: 
 122:   public void write( String text, int offset, int length )
 123:     {   Assert.is_true( text != null                            );
 124:         Assert.is_true( offset >= 0                             );
 125:         Assert.is_true( 0 <= length && length <= text.length()  );
 126: 
 127:         if( log_file == null ) return;  // do nothing if create failed  
 128: 
 129:         try
 130:         {   if( timestamp )
 131:                 log_file.write( new Date().toString() + ": " );
 132: 
 133:             log_file.write( text, offset, length );
 134:             log_file.flush();
 135:         }
 136:         catch( IOException e )
 137:         {   throw new Failure( log_file_name + ": " +
 138:                                                 e.getMessage() );
 139:         }
 140:     }
 141:     /*******************************************************************
 142:      * Convenience method. Logs the entire string.
 143:      */
 144:   public synchronized void write( String text )
 145:     {   this.write( text, 0, text.length() );
 146:     }
 147: 
 148:     /*******************************************************************
 149:      * Close the log file.
 150:      * A Failure is thrown if any exceptions are thrown from
 151:      * the I/O system.
 152:      */
 153:   public synchronized void close( )
 154:     {   if( log_file == null ) return;  // do nothing if create failed  
 155: 
 156:         try
 157:         {   log_file.close();
 158:         }
 159:         catch( IOException e )
 160:         {   throw new Failure( log_file_name + ": " +
 161:                                                 e.getMessage() );
 162:         }
 163:     }
 164: 
 165:     /********************************************************************
 166:      * Objects of the Log.Writer_accessor are returned from the {@link #writer}
 167:      * method. They implement the Writer interface and chain most operations
 168:      * through to the underlying writer. They provide synchronized access to the
 169:      * Writer as well. (That is, multiple threads can safely and simultaneously
 170:      * access both the Log object itself and also any
 171:      * Writers returned from various writer()
 172:      * calls on that Log object). The only surprise is the
 173:      * {@link #close} method, which throws a
 174:      * java.lang.UnsupportedOperationException. You must close the
 175:      * encapsulating Log object, not the object returned from {@link #writer}.
 176:      *
 177:      * Note that:
 178:      *  
 179:      *  (1) The log file is flushed after every write operation (including
 180:      *      single-character writes) to make sure that it's as up-to-date as possible.
 181:      *  (2) If the timestamp() method of the encapsulating Log
 182:      *      has been called, output sent to the Log using either String
 183:      *      version of write() is time stamped, but output sent using
 184:      *      the character versions of write() are not time stamped.
 185:      *  
 186:      */
 187: 
 188:   private class Writer_accessor extends Writer
 189:     {
 190:       public void write( int c ) throws IOException
 191:         {   synchronized(Log.this)
 192:             {   log_file.write(c);
 193:                 log_file.flush();
 194:             }
 195:         }
 196:       public void write( char[] cbuf ) throws IOException
 197:         {   synchronized(Log.this)
 198:             {   log_file.write(cbuf);
 199:                 log_file.flush();
 200:             }
 201:         }
 202:       public void write( char[] cbuf, int off, int len ) throws IOException
 203:         {   synchronized(Log.this)
 204:             {   log_file.write(cbuf,off,len);
 205:                 log_file.flush();
 206:             }
 207:         }
 208:       public void write( String str ) throws IOException
 209:         {   Log.this.write(str);
 210:         }
 211:       public void write( String str, int off, int len ) throws IOException
 212:         {   Log.this.write(str,off,len);
 213:         }
 214:       public void flush() throws IOException
 215:         {   synchronized(Log.this)
 216:             {   log_file.flush();
 217:             }
 218:         }
 219:       public void close()
 220:         {   throw new java.lang.UnsupportedOperationException
 221:                                     ("Must close encasulating Log object");
 222:         }
 223:     }
 224: 
 225:     /** Return a Writer that writes to the current log. This writer is a
 226:      *  singleton in that, for a given Log object, only one
 227:      *  Writer is created. All calls to writer() return
 228:      *  references to the same object.
 229:      */
 230: 
 231:   public Writer writer()
 232:     {   if( the_writer == null )
 233:         {   synchronized( this )
 234:             {   if( the_writer == null )
 235:                     the_writer = new Writer_accessor();
 236:             }
 237:         }
 238:         return the_writer;
 239:     }
 240: 
 241:   private static Writer_accessor the_writer;
 242: 
 243: 
 244:     /**********************************************************************
 245:      * A Unit test class.
 246:      */
 247: 
 248:   public static class Test
 249:   {   public static void main( String[] args )
 250:         {   
 251:             Log log = new Log( "log.test", Std.err() );
 252: 
 253:             log.write( "hello world\n" );
 254:             log.write( "xxxhello world\n", 3, 12 );
 255: 
 256:             log.timestamp();
 257: 
 258:             log.write( "timestamp now on\n" );
 259:             log.write( "xxxhello world\n", 3, 12 );
 260: 
 261:             try
 262:             {   
 263:                 Writer writer = log.writer();
 264:                 writer.write( "output directly to writer\n" );
 265:                 writer.write( new char[]{ 'c', 'h', 'a', 'r', ' ', 'o', 'u', 't', '\n'} );
 266:             }
 267:             catch( IOException exception )
 268:             {   exception.printStackTrace();
 269:             }
 270:         }
 271:     }
 272: }
         

String alignment

String manipulation is one of Java's major weaknesses, both because it's so inefficient and because much of the string-manipulation methods needed by real programs are simply missing. If you're doing hard-core string work, Daniel F. Savarese has generously made his Perl-style regular-expression package available (see Resources), but often a full-blown regular-expression system is overkill. I've created a package of small string-manipulation utilities that might be the subject of a future column, but the one I need for the current calculator application is the Align utility, whose methods align strings within columns.

For example, Align.right( "123", 10 ) returns a 10-character string with "123" right-aligned within it. (The first seven characters of the string are space characters.) The Align utility also supports the Align.left( "123", 10 ) and Align.center( "123", 10 ) methods, which do the expected.

One additional alignment method -- Align.align( "123.45", 10, 5, '.', ' ' ); -- outputs a 10-character string and places the period in "123.45" in column 5. Align uses spaces (specified in the last argument) as fill characters.

The following code:

Std.out().println( "[0123456789]" );
Std.out().println( "[" + Align.align("123.45", 10, 5, '.', ' ') + "]");
Std.out().println( "[" + Align.align("1.2",    10, 5, '.', ' ') + "]");
Std.out().println( "[" + Align.align("12.345", 10, 5, '.', ' ') + "]");

prints:

[0123456789]
[  123.45  ]
[    1.2   ]
[   12.345 ]

The periods are aligned vertically.

The code is in List 8.

List 8. /src/com/holub/string/Align.java
   1: package com.holub.string;
   2: 
   3: public class Align
   4: {
   5:     /*****************************************************************
   6:      * Aligns the text so that the first instance of the align_on character
   7:      * is positioned at align_column, with the pad_character added
   8:      * to both the left and right of the string to make it work.
   9:      * If the align_on character isn't found in the input string, then
  10:      * the output is right adjusted at column alignment_column - 1.
  11:      * For example:
  12:      * 
  13:      *                                             align_column
  14:      *                                                    |
  15:      *                                                    V
  16:      *                                                  01234
  17:      *  align( "1.2"  , 5, 2, '.', '_');    // returns  _1.2_   
  18:      *  align( ".23"  , 5, 2, '.', '_');    // returns  __.23   
  19:      *  align( "12.3" , 5, 2, '.', '_');    // returns  12.3_
  20:      *  align( "2.34" , 5, 2, '.', '_');    // returns  _2.34   
  21:      *  align( "12.34", 5, 2, '.', '_');    // returns  12.34   
  22:      *  align( "1",     5, 2, '.', '_');    // returns  _1___   
  23:      * 
  24:      *
  25:      * @param input         The String to align.
  26:      * @param column_width  The width of the output string.
  27:      * @param align_column  The column at which the align_on character
  28:      *                      is to be positioned.
  29:      * @param align_on      The leftmost instance of this character in
  30:      *                      the input string is aligned at this column.
  31:      *                      If Align.LAST, then the rightmost character in the
  32:      *                      string is used. If Align.FIRST, then the leftmost
  33:      *                      character in the string is used.
  34:      * @param pad_character The character to use for padding.
  35:      * @return The input string, with left and right padding added as needed.
  36:      */
  37: 
  38:    static public int FIRST = 0;
  39:    static public int LAST  = -1;
  40: 
  41:    static public String align( String input, int  column_width
  42:                                              , int  align_column
  43:                                              , int  align_on
  44:                                              , char pad_character )
  45:      {
  46:         int align_character_at = (align_on == FIRST) ? 0                      :
  47:                                  (align_on == LAST ) ? input.length()-1       :
  48:                                                        input.indexOf(align_on);
  49:         int left_padding  = align_character_at >= 0
  50:                             ? Math.max( 0, align_column - align_character_at )
  51:                             : Math.max( 0, align_column - input.length() )
  52:                             ;
  53: 
  54:         int right_padding = column_width - left_padding - input.length();
  55: 
  56:         return do_alignment( left_padding, input, right_padding, pad_character );
  57:     }
  58: 
  59:     /**********************************************************************
  60:      * Build the output string with the required padding.
  61:      */
  62:   static private String do_alignment(int left_padding, String input,
  63:                                     int right_padding, char pad_character)
  64:     {
  65:         StringBuffer work = new StringBuffer();
  66: 
  67:         while( --left_padding >= 0 )
  68:             work.append( pad_character );
  69: 
  70:         work.append( input );
  71: 
  72:         while( --right_padding >= 0 )
  73:             work.append( pad_character );
  74: 
  75:         return work.toString();
  76:      }
  77: 
  78:     /**********************************************************************
  79:      * Convenience method -- pads with spaces, left-adjusting the text.
  80:      */
  81:   static public String left( String input, int column_width )
  82:     {   return align( input, column_width, 0, FIRST, ' ' );
  83:     }
  84: 
  85:     /**
  86:      * Convenience method -- pads with spaces, right-adjusting the text.
  87:      */
  88:   static public String right( String input, int column_width )
  89:     {   return align( input, column_width, column_width-1, LAST, ' ' );
  90:     }
  91: 
  92:     /**
  93:      * Pads the input string with the pad_character so that it
  94:      * is column_width characters wide and centered in the column.
  95:      * If it can't be centered exactly, the extra padding is placed
  96:      * on the left.  For example, if the input string is "abc" and
  97:      * you center it in a 6-character-wide column with '.' as the
  98:      * pad_character, then the output string is "..abc.".
  99:      */
 100:   static public String center( String input, int column_width, char pad_character )
 101:     {   int need = column_width - input.length();
 102:         return ( need > 0 )
 103:                 ? do_alignment( need - (need/2), input, need/2, pad_character )
 104:                 : input
 105:                 ;
 106:     }
 107: 
 108:     /** Convenience method, pads with spaces.
 109:     */
 110: 
 111:   static public String center( String input, int column_width )
 112:     {   return center( input, column_width, ' ' );
 113:     }
 114:       
 115: 
 116:   private static class Test
 117:     {   
 118:       public static void main(String[] args)
 119:         {
 120:         com.holub.tools.Tester t = 
 121:                 new com.holub.tools.Tester( args.length > 0,
 122:                                             com.holub.io.Std.out() );
 123: 
 124:         t.check( "align.1", "+1.2+", align( "1.2"  , 5, 2, '.', '+') );
 125:         t.check( "align.2", "++.23", align( ".23"  , 5, 2, '.', '+') );
 126:         t.check( "align.3", "12.3+", align( "12.3" , 5, 2, '.', '+') );
 127:         t.check( "align.4", "+2.34", align( "2.34" , 5, 2, '.', '+') );
 128:         t.check( "align.5", "12.34", align( "12.34", 5, 2, '.', '+') );
 129:         t.check( "align.6", "+1+++", align( "1",     5, 2, '.', '+') ); 
 130:         t.check( "align.7", "+1.++", align( "1.",    5, 2, '.', '+') ); 
 131: 
 132:         t.check( "align.8", "01234", left ( "01234",    5       )   );
 133:         t.check( "align.9", "     ", left ( "",         5       )   );
 134:         t.check( "align.a", "X    ", left ( "X",        5       )   );
 135: 
 136:         t.check( "align.b", "01234", right( "01234",    5       )   );
 137:         t.check( "align.c", "    X", right( "X",        5       )   );
 138: 
 139:         t.check( "align.d", "  X  ", center("X",        5       )   );
 140:         t.check( "align.e", "  XX ", center("XX",       5       )   );
 141:         t.check( "align.e", "XXXXX", center("XXXXX",    5       )   );
 142:         t.exit();
 143:         }
 144:     }
 145: }
         

Adding scroll bars to a text area.

The final workhorse class of interest to the calculator app is the Scrollable_JTextArea (List 9, line 13).

This is a simple convenience class that compensates for what I consider to be a flaw of Swing's JTextArea: it doesn't create scroll bars when it's too small to fully display its contents. I've provided a small class that extends JScrollPane to automatically create an internal JTextArea. Although the Scrollable_JTextArea is a JScrollPane, it implements many of the methods of JTextArea, which are simple pass-throughs to the contained text widget. That is, you can just pass the Scrollable_JTextArea object messages such as setText(), append(), and getText(), as if it were a JTextArea.

The main problem with making JScrollPane the base class is that, from a design point of view, you really want this thing to be a JTextArea with scroll bars, not a scroll pane. Implementing JScrollPane is awkward. The alternative would be to extend JTextArea and literally override everything, but that solution is both tedious and ugly.

The code is in List 9.

List 9. /src/com/holub/ui/Scrollable_JTextArea.java
   1: package com.holub.ui;
   2: 
   3: import javax.swing.*;
   4: import java.awt.*;
   5: import java.awt.event.*;
   6: 
   7: /**
   8:  *  A convenience class that creates a JTextArea and wraps it into
   9:  *  a JScrollPane for you. A Scrollable_JTextArea is a JScrollPane
  10:  *  that contains a JTextArea.
  11:  */
  12: 
  13: public class Scrollable_JTextArea extends JScrollPane
  14: {
  15:     /******************************************************************
  16:      *  Create an empty Scrollable_JTextArea with scroll bars created as
  17:      *  needed, and word-wrapping on in the encapsulate JTextArea. (The
  18:      *  latter  means that you won't ever get a horizontal scroll bar.
  19:      *  You can change this behavior by sending a setLineWrap(false)
  20:      *  message to the Scrollable_JTextArea object.) The default preferred
  21:      *  size is 250 x 250.
  22:      */
  23: 
  24:   private final JTextArea text = new JTextArea();
  25: 
  26:   public Scrollable_JTextArea( boolean write_only )
  27:     {
  28:         // Set up for line wrap and a vertical scroll bar. Other
  29:         // alternatives could be horizontal and vertical
  30:         // scroll bars and no line wrap.
  31: 
  32:         text.setLineWrap(true);
  33:         text.setWrapStyleWord(true);
  34: 
  35:         setViewportView( text );
  36: 
  37:         setPreferredSize( new Dimension(250, 250) );
  38:         setSize         ( new Dimension(250, 250) );
  39:         setBorder       ( BorderFactory.createEtchedBorder() );
  40: 
  41:         if( write_only )
  42:         {
  43:             text.setEditable  ( false );
  44:             text.addFocusListener
  45:             (   new FocusAdapter()
  46:               {   public void focusGained(FocusEvent e)
  47:                     {   Scrollable_JTextArea.this.requestFocus();
  48:                     }
  49:                 }
  50:             );
  51:         }
  52:     }
  53: 
  54:     /******************************************************************
  55:      * Convenience method, sets the initial text to initial_text.
  56:      */
  57:   public Scrollable_JTextArea( String initial_text, boolean write_only )
  58:     {   this(write_only);
  59:         text.setText( initial_text );
  60:     }
  61: 
  62:     /******************************************************************
  63:      * Convenience method, sets the initial text to initial_text, one
  64:      * String per line. The initial font is 12-point Monospaced.
  65:      * The size is controlled by the size of the text. The number of
  66:      * columns is the maximum number of characters on a line in
  67:      * initial-text; the number of rows is the number of lines.
  68:      * Word wrapping is off.
  69:      */
  70: 
  71:   public Scrollable_JTextArea( String[] initial_text, boolean write_only )
  72:     {   this(write_only);
  73:         text.setLineWrap( false                                  );
  74:         text.setFont    ( new Font("Monospaced", Font.PLAIN, 12) );
  75:         text.setRows    ( initial_text.length                    );
  76: 
  77:         int width  = 0;
  78:         for( int i = 0 ; i < initial_text.length; ++i )
  79:         {   String current = initial_text[i];
  80:             width = Math.max( width, current.length() );
  81:             text.append( current + "\n" );
  82:         }
  83: 
  84:         text.setColumns( width );
  85: 
  86:         Dimension size = text.getPreferredSize();
  87:         size.height += 5;   // Add some slop so that scroll bars won't
  88:         size.width  += 5;   // be displayed initially.
  89: 
  90:         setPreferredSize( size );
  91:         setSize         ( size );
  92:     }
  93: 
  94:     /******************************************************************
  95:      * Convenience methods, just chain through to the
  96:      * setText() method of the contained JTextArea();
  97:      */
  98: 
  99:   public final void setText( String str )
 100:     {   text.setText( str );
 101:     }
 102: 
 103:   public final void append( String str )
 104:     {   text.append( str );
 105:     }
 106: 
 107:   public final void setEditable(boolean on)
 108:     {   text.setEditable(on); 
 109:     }
 110: 
 111:   public final void setLineWrap(boolean on)
 112:     {   text.setLineWrap(on);
 113:     }
 114: 
 115:   public final String getText()
 116:     {   return text.getText();
 117:     }
 118: 
 119:   public final void setBackground(Color c)
 120:     {  if(text!=null) text.setBackground(c);
 121:     }
 122: 
 123:   public final void setForeground(Color c)
 124:     { if(text!=null) text.setForeground(c);
 125:     }
 126: 
 127:   public final void setFont(Font f)
 128:     { if(text!=null) text.setFont(f);
 129:     }
 130: 
 131:     /******************************************************************
 132:      * Accessor method, provides access to the contained JTextArea
 133:      * if you need it.
 134:      */
 135:   public final JTextArea getTextArea( ){ return text; }
 136: 
 137:     /******************************************************************
 138:      * Container of the unit-test
 139:      */
 140:   static public class Test
 141:     {
 142:       static public void main( String[] args )
 143:         {   test1();
 144:             test2();
 145:         }
 146: 
 147:         static void test1()
 148:         {
 149:             Scrollable_JTextArea text = new Scrollable_JTextArea(
 150:                     "This is an editable JTextArea " +
 151:                     "that has been initialized with the setText method. " +
 152:                     "A text area is a \"plain\" text component, " +
 153:                     "which means that although it can display text " +
 154:                     "in any font, all of the text is in the same font.",
 155:                     false
 156:             );
 157: 
 158:             JFrame frame = new JFrame();
 159:             frame.getContentPane().add( text );
 160:             frame.setVisible( true );
 161: 
 162:             try{ Thread.currentThread().sleep(10000); } catch (Exception e){}
 163: 
 164:             text.setText(   "Here is a completely new\n"+
 165:                             "chunk of text\n"+
 166:                             "this time\n" +
 167:                             "with newlines."
 168:                         );
 169:         }
 170: 
 171:         static void test2()
 172:         {   String[] contents = { "This is a read-only ->",
 173:                                   "multiline text control ->",
 174:                                   "that should display->",
 175:                                   "four lines.->"
 176:                                 };
 177: 
 178:             Scrollable_JTextArea text = new Scrollable_JTextArea( contents,true );
 179:             JFrame frame = new JFrame();
 180:             frame.getContentPane().add( text );
 181:             frame.pack();
 182:             frame.setVisible( true );
 183:         }
 184:     }
 185: }
         

Loose ends

The following classes were covered in depth in previous articles, but they're used either in the foregoing code or by the calculator itself, so I've reprinted them here for the sake of completeness.

List 10. /src/com/holub/asynch/JDK_11_unloading_bug_fix.java
   1: package com.holub.asynch;
   2: 
   3: /**
   4:  * <table border=1 cellspacing=0 cellpadding=5><tr><td><font size=-1>  5:  * <center>(c) 1999, Allen I. Holub.</center>
   6:  * <p>
   7:  * This code may not be distributed by yourself except in binary form,
   8:  * incorporated into a java .class file. You may use this code freely
   9:  * for personal purposes, but you may not incorporate it into any
  10:  * commercial product without express permission of Allen I. Holub in
  11:  * writing.
  12:  * </td></tr></table>
  13:  *
  14:  *  <p>
  15:  *  This class provides a workaround for a bug in the JDK 1.1 VM that
  16:  *  unloads classes too aggressively. The problem is that if the only
  17:  *  reference to an object is held in a static member of the object, the
  18:  *  class is subject to unloading and the static member will be discarded.
  19:  *  This behavior causes a lot of grief when you're implementing a
  20:  *  Singleton. Use it like this:
  21:  *
  22:  *  
  23:  *  class Singleton
  24:  *  {   private Singleton()
  25:  *      {   new JDK_11_unloading_bug_fix(Singleton.class);
  26:  *      }
  27:  *      // ...
  28:  *  }
  29:  *  
  30:  *  In either event, once the "JDK_11_unloading_bug_fix" object is
  31:  *  created, the class (and its static fields) won't be unloaded for
  32:  *  the life of the program.
  33:  */
  34: 
  35: public class JDK_11_unloading_bug_fix
  36: { 
  37:   public JDK_11_unloading_bug_fix( final Class the_class )
  38:     {   
  39:         if( System.getProperty("java.version").startsWith("1.1") )
  40:         {
  41:             Thread t =
  42:                 new Thread()
  43:               {   private Class singleton_class = the_class;
  44:                   public void run()
  45:                     {   synchronized(this)
  46:                         {   try{ wait(); }catch(InterruptedException e){}
  47:                         }
  48:                     }
  49:                 };
  50: 
  51:             // I'm not exactly sure why synchronization is necessary, below,
  52:             // but without it, some VMs complain of an illegal-monitor
  53:             // state. The effect is to stop the wait() from executing
  54:             // until the thread is fully started.
  55: 
  56:             synchronized( t )
  57:             {   t.setDaemon(true);  // so program can shut down
  58:                 t.start();
  59:             }
  60:         }
  61:     }
  62: }
         
List 11. /src/com/holub/ui/AncestorAdapter.java
   1: package com.holub.ui;
   2: 
   3: import javax.swing.event.*;
   4: 
   5: /** Corrects a flaw in Swing, provides an AncestorListener
   6:  *  implementation made up of empty methods.
   7:  */
   8: 
   9: public class AncestorAdapter implements AncestorListener
  10: {   public void ancestorAdded   ( AncestorEvent event ){}
  11:   public void ancestorMoved   ( AncestorEvent event ){}
  12:   public void ancestorRemoved ( AncestorEvent event ){}
  13: }
         
Allen Holub runs Holub Associates in the San Francisco Bay Area. The firm designs software and guides you through its implementation in much the same way that traditional architects design buildings and help contractors construct them. Holub Associates also provides training in object-oriented design and Java, provides design-review and mentoring services, and does occasional Java implementations. Allen has been working in the computer industry since 1979. He is widely published in magazines (Dr. Dobb's Journal, Programmers Journal, Byte, and MSJ, among others). He is working on his eighth book, which will present the complete sources for a Java compiler written in Java. A former C++ programmer, Allen abandoned it for Java in early 1996. He now looks at C++ as a bad dream, the memory of which is mercifully fading. He has built two operating systems from scratch, several compilers, and various application programs ranging in scope from robotics controllers to children's software. He has been teaching programming, both on his own and for the University of California Berkeley Extension, since 1982. Get information and contact Allen via his Website http://www.holub.com.

Learn more about this topic

The complete Java Toolbox archive