JTF > Rationale > The acm.program Package

Chapter 6
The acm.program Package


In the taxonomy of problems presented in Chapter 3, one of the issues that rose to the fore (as part of the discussion under problem L1) is that Java applications start by invoking a method whose signature is

 

public static void main(String[] args)

The structure of the main method in an application has several negative effects. One of the most commonly cited is that the method signature for main includes several concepts—static methods and array parameters, for example—that are difficult to present to novices. This shortcoming, however, is only part of the problem. The Java Task Force views the following problems as being of at least as much concern:

To minimize these negative effects, the Java Task Force offers a new Program class as part of its standard library packages. The central idea behind the proposal is simply that the existence of such a class—coupled with several specific subclasses that define specific program types—gives instructors and students a standard framework for creating better, more functional applications.

We believe that the facilities offered by the acm.program package will have a profoundly simplifying effect on introductory Java examples. The major advantages associated with the use of this package can be summarized as follows:

  1. The conventional pattern of use associated with the acm.program package moves students away from the procedural style of public static void main into a more pedagogically defensible environment in which students are always working in the context of an object.

  2. The Program class allows Java applications to double as applets, thereby making it possible for instructors to use either paradigm in a consistent way. Even for those instructors that choose to focus on the application paradigm, using the Program class makes it much easier for students to make their code available on the web. Moreover, because the Program class is defined to be a subclass of JApplet (and indirectly therefore from the basic Applet class) applications that extend Program can take advantage of such applet-based features as loading audio clips and images from a code base.

  3. The Program class includes several features that make instruction easier, such as menu bars that support operations like printing and running programs with an automated test script.

  4. The classes in the acm.program package offer a compelling example of an inheritance hierarchy that introductory students can understand and appreciate right from the beginning of their first course.

As is often the case, however, these advantages are much easier to understand if one has some familiarity with the general idea of the proposal. This chapter, therefore, begins by illustrating the Program class with a few simple examples, and then describes in more detail the subclasses available in the standard acm.program hierarchy.

6.1 Simple examples of the Program class

The acm.program package defines a small hierarchy of classes, as illustrated in Figure 6-1. Every class in the package is an abstract class and is therefore instantiated only in the form of a specific client-defined subclass. A student who writes a program using this package picks the Program subclass most appropriate for the problem at hand and then writes a new class definition that extends that base.

Figure 6-1. Class diagram for the acm.program package

As an example, Figure 6-2 shows a program that descends directly from the Program class and has the effect of reading two integers from the user and displaying their sum. Because this program makes very little use of the object-oriented paradigm, it is by no means representative of the program that the Java Task Force encourages, but will nonetheless serve as a useful baseline for illustrating the operation of the class.

Figure 6-2. Simple program to add two numbers
/*
 * File: Add2.java
 * --------------- 
 * This program adds two numbers and prints their sum.  Because
 * this version is a Program, input and output are assigned to
 * System.in and System.out.
 */
 
import acm.program.*;
 
public class Add2 extends Program {
 
   public void run() {
      println("This program adds two numbers.");
      int n1 = readInt("Enter n1: ");
      int n2 = readInt("Enter n2: ");
      int total = n1 + n2;
      println("The total is " + total + ".");
   }
 
} 

The Add2 class in Figure 6-2 defines a single method called run, which specifies the code that needs to be executed when the program is run. In this example, the run method reads two integers, adds them together, and displays the result. Because this implementation is a direct subclass of Program, input is read from the standard input stream (System.in) and output appears on the standard output stream (System.out). No windows are created by the Program class itself, which means that the Add2 program might produce the following typescript in a command-line environment, shown here as it appears in a terminal window running on Mac OS X:

The Program subclasses at the bottom of Figure 6-1 make it possible to change the behavior of this program in relatively small but highly useful ways. If the first line of the class definition is changed to

 

public class Add2 extends ConsoleProgram 

then the same program runs in a window that contains an IOConsole as defined in Chapter 4. That program might look like this if it were run under Windows XP:

If the definition of the Add2 class were instead changed to extend DialogProgram, the program would then operate by putting up a series of dialog boxes, as illustrated in Figure 6-3 using the Mac OS X style. (The GraphicsProgram class is not appropriate to the add-two-numbers example and will be discussed in section 6.4.)

Figure 6-3. Dialogs produced by the DialogProgram version of Add2
     

6.2 Programs as a paradigm for inheritance

The primary point of introducing the examples in the preceding section is to illustrate an important pedagogical advantage of the acm.program class hierarchy. One of the challenges that instructors face in using an objects-first approach is finding examples of class hierarchies that make sense in the programming domain. The real world has no shortage of such examples. The classification of the biological kingdom, for example, offers a wonderful model of the class idea and the notion of subclassing. An individual horse is an instance of the general class of horses, which is itself a subclass of mammal, which is in turn a subclass of animal. Students have no trouble understanding the subclass relationship in the biological context: all horses are mammals, all mammals are animals, but there are some mammals that are not horses and some animals that are not mammals.

So far, so good. The trouble for teachers trying to offer new students a compelling justification for object-oriented design comes in trying to reflect the intuitive notion of class hierarchies in a programming context. Textbooks often struggle to find appropriate, programming-related examples. Most end up relying on a graphical hierarchy similar to the one outlined in Chapter 5. That hierarchy provides examples that are similar to the biological model: a GStar (as defined in Figure 5-11) is a subclass of GPolygon, which is in turn a subclass of GObject. The structure of the graphics classes supports the intuition that students derive from real-world hierarchies: all stars are polygons, all polygons are graphical objects, but not all graphical objects are polygons, let alone stars.

While the hierarchy from acm.graphics works well as an example, the Task Force believes that it is possible to illustrate this same idea even earlier by making the intuitive notion of a “program” into a hierarchy in its own right. Students are then exposed to the notions of objects, classes, and subclass hierarchies as the first concepts they encounter. By establishing a hierarchy of programs, students can see how their particular program is an instance of a specific program category, which is itself a subcategory of a larger, more generic class of programs. In particular, students will quickly come to understand that every ConsoleProgram is also a Program and can perform the operations that a Program does, subject to any extensions at the ConsoleProgram level. This understanding of the fundamental is-a relationship should help to emphasize the idea of inheritance.

6.3 The structure of the acm.program package

The most important thing to understand about the design of the acm.program package is the merging of the applet and application roles. The Program class extends JApplet, and is therefore available for use by a web browser. When invoked as an applet, the browser instantiates an instance of the main applet class and invokes its init method. Each Program class, however, also includes an implementation of a method with the familiar signature

 

public static void main(String[] args)

which allows it to be invoked as an application. As part of its startup operation, the application version of a Program also calls the init method, which makes it possible to include initialization code in a way that is analogous to the applet mode of operation. The main method in the Program class identifies the class that invoked it, instantiates a new instance of that class, and then calls the init and start methods of that program, just as a browser running an applet would do. Thus, in both the applet and application frameworks, the program runs in the context of an instantiated object rather than in the static domain.

Irrespective of whether the program is invoked as an applet or application, the initialization code establishes a component hierarchy that matches the one used in the Swing JApplet class. Moreover, as in Java 2 Standard Edition 5.0, the add methods for the Program class automatically forward any add requests to the content pane, so that it is now possible to write

 

add(component);

instead of the novice-unfriendly

 

getContentPane().add(component);

By default, the Program class itself doesn’t add anything to the content pane, although subclasses are free to do so. If the content pane has any components at the end of the initialization phase, the implementation calls setVisible(true) on the program frame to display it on the screen. If the content pane is empty, that call does not occur. Thus, programs that are written to use only the standard input streams or popup dialogs for their user interactions can run without displaying a window.

Program startup

After setting up the standard JApplet component hierarchy, the Program startup code executes the following three steps:

  1. Calls the init method to initialize the application.
  2. Calls validate on the content pane to ensure that its components are arranged correctly on the screen.
  3. Creates a new thread to execute the run method.

Although it can be useful to include both methods in a single program, most student programs will define either an init method or a run method, but not both. The init method is used for interactive applications in which the program creates and arranges a set of components and then waits for the user to trigger some action, typically by clicking on a button. The run method is intended for programs that have an independent thread of control unrelated to the actions of the user. Programs that animate a graphical object or require console interaction are usually of this form.

Consider, for example, the Add2 program presented at the beginning of this chapter. That program establishes a dialog with the user through the console that requires sequential operation and therefore requires a run method, which for the Add2 program looks like this:

 

public void run() {
   println("This program adds two numbers.");
   int n1 = readInt("Enter n1: ");
   int n2 = readInt("Enter n2: ");
   int total = n1 + n2;
   println("The total is " + total + ".");
}

The run method in a Program is analogous to the main method in a Java application. The key difference is that the run method is not static and therefore has an object to work with. It can use instance variables in that object and can invoke other methods that are not themselves static. This change alone represents the central advantage of the acm.program package.

The rationale behind the init method

The February 2005 release of the acm.program package did not describe the init method, although it was there as part of the standard applet paradigm. At the time, we proposed that initialization occur in a constructor for the class. That strategy, however, did not prove viable. The constructor for a program is invoked very early and therefore has no access to any contextual information. Inside the constructor, for example, it is impossible to determine the dimensions of the applet frame, to perform any operations that require a code base such as loading an image, or even to determine whether one is running in an application or applet context. Students found these restrictions very difficult to understand. The init method is invoked after the object has been instantiated and installed in the frame, which means that the program has access to the required information.

Public methods in the Program class

A list of the public methods in the Program class appears in Figure 6-4. A large fraction of these methods implement the IOModel interface or one of the event interfaces from java.awt.event. To save space in the table, these methods are not described in full. The details of the IOModel methods are described in Chapter 4.

Figure 6-4. Public methods common to all Program objects
Methods involved in running a program
 
void init()
  Initializes the program; GUIs are typically created here.
void run()
  Specifies the code for the program, which is run in a separate thread.
 
Methods for setting and retrieving the I/O context of the program
 
void getConsole(IOConsole console)
  Sets the IOConsole assigned to this program.
IOConsole getConsole()
  Returns the IOConsole assigned to this program.
IODialog getDialog()
  Returns the IODialog assigned to this program.
PrintWriter getWriter()
  Returns a writer that can be used to write to the console.
BufferedReader getReader()
  Returns a reader that can be used to read from the console.
IOModel getInputModel()
  Returns the input model, which is typically either the standard console or dialog.
IOModel getOutputModel()
  Returns the output model, which is typically either the standard console or dialog.
 
Methods for manipulating layout regions of the program
 
void setLayout(String region, LayoutManager manager)
  Sets the layout manager for the specified program region.
LayoutManager getLayout(String region)
  Returns the layout manager for the specified program region.
void removeAll(String region)
  Removes all the components from the specified program region.
 
Miscellaneous methods
 
void pause(double milliseconds)
  Sleeps for the specified number of milliseconds but never throws an exception.
void exit()
  Exits from the program.
void setTitle(String title)
  Sets the title used for the application title bar.
String getTitle()
  Returns the title used for the application title bar.
boolean isAppletMode()
  Returns true if this applet is running in a browser (not available in the constructor).
void addActionListeners()
  Adds the Program as an ActionListener to every button it contains.
 
Methods specified by the IOModel interface
 
void print(...)
void println(...)
String readLine(...)
int readInt(...)
double readDouble(...)
boolean readBoolean(...)
void showErrorMessage(String msg)
 
Methods specified by the MouseListener, MouseMotionListener, and ActionListener interfaces
 
void mouseClicked(MouseEvent e)
void mousePressed(MouseEvent e)
void mouseReleased(MouseEvent e)
void mouseEntered(MouseEvent e)
void mouseExited(MouseEvent e)
void mouseMoved(MouseEvent e)
void mouseDragged(MouseEvent e)
void actionPerformed(ActionEvent e)

Input and output from programs

Every Program object is given both an IOConsole and IODialog object at initialization time. By default, the console is IOConsole.SYSTEM_CONSOLE and the dialog is a default instance of IODialog. Subclasses can substitute specially tailored instances of these classes by overriding the protected factory methods createConsole and createDialog. The ConsoleProgram subclass, for example, overrides createConsole as follows:

 

protected IOConsole createConsole() {
   return new IOConsole();
}

The Program class also defines two public methods—getInputModel and getOutputModel—to specify the source and destination for the input and output methods, respectively. In the Program class itself, these methods return the result of getConsole. The DialogProgram subclass overrides these methods to return the result of getDialog instead. Indeed, those two one-line methods are the only code in the DialogProgram subclass.

The varieties of object-oriented experience

Depending on your perception of what properties make a program object-oriented, the code for the Add2 example presented earlier in this chapter may not seem to meet those requirements. The method calls in the body of the run method have no explicit receivers, depending instead on the fact that the Program class itself implements the IOModel interface and therefore defines methods like println and readInt. Such an approach is simpler for novices—as well as for teachers with little background in the object-oriented style of programming—but it doesn’t emphasize the idea of sending messages to objects. It is possible, however, to rewrite the program so that make those object references explicit. If an instructor chooses to have all method calls be tied to an object, the run method for Add2 could be rewritten like this:

 

public void run() {
   IOModel io = this.getConsole();
   io.println("This program adds two numbers.");
   int n1 = io.readInt("Enter n1: ");
   int n2 = io.readInt("Enter n2: ");
   int total = n1 + n2;
   io.println("The total is " + total + ".");
}

Judging from the range of opinions on the Task Force, different instructors will have strong preferences for one or the other of these paradigms, and it does not seem wise to allow only one of these styles. The decision to support both approaches is also consistent with the first principle put forward in Chapter 2, in which we make a commitment “to ensure that our library packages and tools are usable with other pedagogical approaches” besides the objects-first approach.

6.4 The GraphicsProgram subclass

The GraphicsProgram subclass simplifies the use of the acm.graphics package from Chapter 5 and is quite straightforward in its design. During initialization, a GraphicsProgram allocates a new GCanvas and installs it in the content pane of the Program window. Subclasses that extend GraphicsProgram can then use either of two strategies to gain access to that canvas:

  1. Use the getGCanvas method to obtain the GCanvas and then invoke methods directly on that canvas.

  2. Invoke wrapper methods in the GraphicsProgram class, each of which performs that delegation automatically.

The first paradigm is illustrated by the following program, which draws the string “Hello, world.” at the center of the program window:

 

public class Hello extends GraphicsProgram {
   public void run() {
      GCanvas gc = this.getGCanvas();
      GLabel label = new GLabel("Hello, world.");
      gc.add(label, (this.getWidth() - label.getWidth()) / 2,
                     this.getHeight() / 2);
   }
}

The second paradigm is simpler but uses a seemingly less object-oriented style, given that the receivers of the messages are all implicit:

 

public class Hello extends GraphicsProgram {
   public void run() {
      GLabel label = new GLabel("Hello, world.");
      add(label, (getWidth() - label.getWidth()) / 2,
                  getHeight() / 2);
   }
}

The GraphicsProgram class implements wrappers for all the methods in GCanvas, with the exception of the overloaded version of the add method that takes a Component instead of a GObject. That method would have an ambiguous interpretation, because there would be no way to differentiate the operation of adding a component to the GCanvas or adding that component to the content pane of the program. This method is unlikely to be used by very many students and can easily be invoked unambiguously by calling

 

getGCanvas().add(component);

Responding to events in a GraphicsProgram

The GraphicsProgram class supports several strategies for responding to mouse events:

  1. Use the GraphicsProgram itself as a listener for mouse events that occur within the embedded GCanvas. To do so, all the student has to do is define any listener methods to which the program needs to respond and then call addMouseListeners(), which registers the program as both a MouseListener and MouseMotionListener.

  2. Add mouse listeners explicitly to the embedded GCanvas. This strategy corresponds most closely to the way event listeners are specified in modern Java applications. In the coding style that has become widespread since the introduction of Swing in JDK 1.2, those listeners are often specified using anonymous inner classes defined as part of the call that adds the listener, as illustrated in Figure 6-6. The problem with this approach at the introductory level is that it is difficult to explain the semantics of inner classes to novices, particular if interactivity is introduced early in an introductory course.

  3. Add listeners to the individual GObjects. In this model, each GObject that is assigned a listener becomes an interactor and becomes the source for mouse events that occur within its borders. This strategy is useful if all interaction takes place in the context of a GObject, as opposed to those applications in which it is also important to field events that occur in the background canvas. This model is based on the event-handling framework illustrated in Figure 5-8 from Chapter 5.

  4. Define callback handlers in the GraphicsProgram to handle specific event types. As a further simplification intended to make mouse interaction as simple as possible for new students, the GraphicsProgram class supports an additional strategy derived from objectdraw [Bruce04a]. If the program class defines any of the methods

     
    
    mousePressed(GPoint pt)
    mouseReleased(GPoint pt)
    mouseClicked(GPoint pt)
    mouseMoved(GPoint pt)
    mouseDragged(GPoint pt)
    

    then that method is called whenever the appropriate event occurs in the GCanvas. The argument is the point at which the event occurred, already translated into the real-valued coordinate space of the acm.graphics package. This model hides the details of mouse events and mouse listeners, which is either a strength or a weakness depending on your point of view. On the one hand, this model makes it easier for students to write interactive graphical programs early because so much of the underlying complexity is hidden. On the other, students who use this style will have to learn how standard Java listeners work at some later point in their studies.

Each of these event-handling models is illustrated by one of the code examples in Figures 6-5, 6-6, 6-7, and 6-8, which reimplement the object-dragging example from Chapter 5 using one of these models.

Figure 6-5. Object-dragging program using the addMouseListeners method
/*
 * File: DragObjects.java
 * ----------------------
 * This implementation illustrates the technique of using the
 * addMouseListeners method to register the program itself as
 * a listeners for events in the underlying GCanvas.
 */
 
import java.awt.*;
import java.awt.event.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragObjects extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      add(oval);
      addMouseListeners();
   }
 
/** Called on mouse press to record the coordinates of the click */
   public void mousePressed(MouseEvent e) {
      last = new GPoint(e.getPoint());
      gobj = getElementAt(last);
   }
 
/** Called on mouse drag to reposition the object */
   public void mouseDragged(MouseEvent e) {
      if (gobj != null) {
         gobj.move(e.getX() - last.getX(), e.getY() - last.getY());
         last = new GPoint(e.getPoint());
      }
   }
 
/** Called on mouse click to move this object to the front */
   public void mouseClicked(MouseEvent e) {
      if (gobj != null) gobj.sendToFront();
   }
 
/* Private instance variables */
   private GObject gobj;           /* The object being dragged */
   private GPoint last;            /* The last mouse position  */

Figure 6-6. Object-dragging program using inner classes to specify the listeners
/*
 * File: DragUsingInnerClasses.java
 * --------------------------------
 * This implementation illustrates the technique of defining
 * listeners as anonymous inner classes.
 */
 
import java.awt.*;
import java.awt.event.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragUsingInnerClasses extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      add(oval);
      GCanvas canvas = getGCanvas();
      canvas.addMouseListener(new MouseAdapter() {
         public void mousePressed(MouseEvent e) {
            last = new GPoint(e.getPoint());
            gobj = getElementAt(last);
         }

         public void mouseClicked(MouseEvent e) {
            if (gobj != null) gobj.sendToFront();
         }
      });
      canvas.addMouseMotionListener(new MouseMotionAdapter() {
         public void mouseDragged(MouseEvent e) {
            if (gobj != null) {
               gobj.move(e.getX() - last.getX(),
                         e.getY() - last.getY());
               last = new GPoint(e.getPoint());
            }
         }
      });
   }
 
/* Private instance variables */
   private GObject gobj;           /* The object being dragged */
   private GPoint last;            /* The last mouse position */

Figure 6-7. Object-dragging program that listens to the GObjects
/*
 * File: DragUsingGObjectEvents.java
 * ---------------------------------
 * This implementation illustrates the technique of assigning
 * listeners to GObjects.
 */
 
import java.awt.*;
import java.awt.event.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragUsingGObjectEvents extends GraphicsProgram {
 
/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      rect.addMouseListener(this);
      rect.addMouseMotionListener(this);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      oval.addMouseListener(this);
      oval.addMouseMotionListener(this);
      add(oval);
   }
 
/** Called on mouse press to record the coordinates of the click */
   public void mousePressed(MouseEvent e) {
      last = new GPoint(e.getPoint());
   }
 
/** Called on mouse drag to reposition the object */
   public void mouseDragged(MouseEvent e) {
      GObject gobj = (GObject) e.getSource();
      gobj.move(e.getX() - last.getX(), e.getY() - last.getY());
      last = new GPoint(e.getPoint());
   }
 
/** Called on mouse click to move this object to the front */
   public void mouseClicked(MouseEvent e) {
      GObject gobj = (GObject) e.getSource();
      gobj.sendToFront();
   }
 
/* Private instance variables */
   private GPoint last;            /* The last mouse position */

Figure 6-8. Object-dragging program that uses callback methods
/*
 * File: DragUsingObjectDrawModel.java
 * -----------------------------------
 * This implementation illustrates the technique of using callback
 * methods in the style of the objectdraw package.
 */
 
import java.awt.*;
import acm.graphics.*;
import acm.program.*;
 
/** This class displays a mouse-draggable rectangle and oval */
public class DragUsingObjectDrawModel extends GraphicsProgram {

/** Initializes the program */
   public void init() {
      GRect rect = new GRect(100, 100, 150, 100);
      rect.setFilled(true);
      rect.setColor(Color.RED);
      add(rect);
      GOval oval = new GOval(300, 115, 100, 70);
      oval.setFilled(true);
      oval.setColor(Color.GREEN);
      add(oval);
   }
 
/** Called on mouse press to record the coordinates of the click */
   public void mousePressed(GPoint pt) {
      last = pt;
      gobj = getElementAt(last);
   }
 
/** Called on mouse drag to reposition the object */
   public void mouseDragged(GPoint pt) {
      if (gobj != null) {
         gobj.move(pt.getX() - last.getX(), pt.getY() - last.getY());
         last = pt;
      }
   }
 
/** Called on mouse click to move this object to the front */
   public void mouseClicked(GPoint pt) {
      if (gobj != null) gobj.sendToFront();
   }
 
/* Private instance variables */
   private GObject gobj;           /* The object being dragged */
   private GPoint last;            /* The last mouse position  */
}
 

Even though the acm.graphics package supports each of these models, it would almost certainly be a mistake to teach all four options to students, particularly at the introductory level. Instead, it makes pedagogical sense to presents only one of these strategies so that students are not confused by multiple approaches for solving similar tasks. The reason for providing the different mechanisms is to support different teaching styles, and there was no consensus as to which approach would be best for novices. Each of the strategies has distinct advantages and disadvantages, and we therefore thought it was prudent to support each strategy and to let individual instructors choose a style appropriate to their approach to the material.

From the feedback we have received from users who have worked with the beta version of the acm.graphics package, it is clear that the strategy of using the program as the listener for mouse events works well at the introductory level. Based on that successful experience, we decided to add a parallel facility to support keyboard events. Calling addKeyListeners() in a GraphicsProgram adds the program as a key listener to the embedded canvas.

6.5 Layout strategy for the Program class

Much of the development work between the first and second draft releases of the Java Task Force packages was directed toward finding strategies to simplify the creation of graphical user interfaces, or GUIs. The results of that work and the rationale that led up to it are outlined in Chapter 7, which describes the acm.gui package. One aspect of that design, however, seemed to fit more easily in the acm.program package, because it is logically associated with the Program class itself. The essential idea behind the design is that programs often need to be able to create simple control bars that present the user with a simple list of interactors. Unfortunately, given the facilities provided by the standard Java packages, building this type of control bar and populating it with interactors is often too difficult for novices. As at least a partial solution to this problem, the Java Task Force decided to implement a new layout manager for programs whose primary function is to simplify the creation of control bars.

The Program class automatically assigns a BorderLayout manager to the content pane and initializes each of the five components—NORTH, SOUTH, EAST, WEST, and CENTER—to be an empty JPanel. The individual JPanel objects are arranged as in the standard BorderLayout paradigm, which looks like this:

Each of the regions along the edges is assigned its preferred size, which means that it disappears entirely as long as it is empty. The center region gets all the remaining space.

The Program class overrides the standard definitions of the add methods so that components are added to the center region by default, but to one of the side regions if the constraint matches the appropriate region constant. Thus, calling

 

add(new JButton("OK"), SOUTH);

will add a new button labeled OK to the SOUTH region.

As with any container, the components that makes up the regions of a Program object are rearranged whenever the container is validated, which happens automatically when the container is resized but which can also be triggered explicitly by calling validate on the container. The Program logic automatically calls validate after the init method returns, which means that most students will never have to worry about forcing an update of the layout structure.

Each of the JPanels that form the five program regions has a default layout manager, as follows:

NORTH, SOUTHnew TableLayout(1, 0, 5, 5)Single horizontal row.
EAST, WESTnew TableLayout(0, 1, 5, 5)Single vertical column.
CENTERnew GridLayout(1, 0)Equally spaced horizontal row.

You can, however, change any of these layout managers by calling

 

setLayout(region, layout);

The details of the TableLayout class are discussed in Chapter 7.

Assigning action listeners to buttons

The most common use of the border regions defined by the Program class is to create a control strip by adding one or more JButtons to one of the regions. Creating the buttons, however, is only part of the process. To make the buttons active, it is also necessary to assign an action listener to each button so that pressing the button performs the appropriate action.

It is, of course, possible to adopt the style that has become commonplace in professional Java coding and assign an individual action listener to each button in the form of an anonymous inner class. For example, if you wanted to create a Test button whose effect was to invoke a method called testAction, you could do so by writing the following code:

 

JButton testButton = new JButton("Test");
testButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent e) {
      testAction();
   }
});

Although there are certainly instructors who favor this style even at the level of introductory courses, the members of the Task Force were concerned that such code introduces too many unfamiliar concepts for novice programmers to comprehend. To simplify the structure and eliminate the use of inner classes, the Task Force chose to define the Program class itself as an action listener and allow students to define the actionPerformed method as part of their program code. Adopting this strategy means that the definition of the button can be shortened to

 

JButton testButton = new JButton("Test");
testButton.addActionListener(this);

The actual dispatch is specified in a separate actionPerformed method, as follows:

 

public void actionPerformed(ActionEvent e) {
   if (e.getActionCommand().equals("Test")) {
      testAction();
   }
}

This single actionPerformed method is shared among all the buttons defined in this way, which means that it must perform internal tests to determine which button has been activated. That determination can be made either by looking at the action command (as shown here) or by calling e.getSource to obtain the identity of the button that triggered the event.

As a further simplification, the Program class includes a method called addActionListeners, which recursively traverses the components displayed on the screen and adds the program as an action listener for every JButton it encounters. This style is illustrated in the examples shown in the section that follows.

A simple example

The code in Figure 6-9 offers a simple example of the layout strategy provided by the Program class. The code defines three buttons that simply display their name on the console whenever they are activated.

Figure 6-9. A ConsoleProgram version of a stoplight
/*
 * File: StoplightConsole.java
 * ---------------------------
 * This program illustrates the construction of a simple GUI.
 */
 
import acm.program.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
/**
 * This class displays three buttons at the south edge of the window.
 * The name of the button is echoed on the console each time a button
 * is pressed.
 */
 
public class StoplightConsole extends ConsoleProgram {
 
/** Initialize the buttons. */
   public void init() {
      add(new JButton("Green"), SOUTH);
      add(new JButton("Yellow"), SOUTH);
      add(new JButton("Red"), SOUTH);
      addActionListeners();
   }
 
/** Listen for a button action. */
   public void actionPerformed(ActionEvent e) {
      println(e.getActionCommand());
   }

}
 
 

The three calls to add in the constructor assign these buttons to the SOUTH region of the layout, as specified by the constraint parameter in the add call. Because the default layout for the SOUTH region is a TableLayout manager with a single row and a five-pixel gap between adjacent interactors. The result is therefore a program window that looks like this, which shows the state of the console window after the user has clicked each of the buttons in order:

Positioning interactors around the border is equally useful with Program subclasses other that ConsoleProgram. The code on the web site for the stoplight program also includes a graphical version that makes the program a bit more interesting.

6.6 Using menu bars with the Program class

Applications that are running in a modern window-based environment typically include menu bars to give the user ready access to a variety of useful operations, typically including both file-based operations such as saving or printing and editor-based operations such as cut, copy, and paste. The ProgramMenuBar class is designed to support the inclusion of menu bars in any of the Program subclasses. Every Program instance includes a standard menu bar by default, and it is easy to extend that menu bar to include application-specific menu items.

The default menu bar

By default, the menu bar associated with each program includes a File and an Edit menu, which have the structure shown in Figure 6-10. The File menu includes the following items:

The Edit menu includes items for the Cut, Copy, Paste, and Select All options, all of which are standard in modern computing platforms.

Figure 6-10. Menus provided by the standard Program class
  The menu bars for a Program follow the standard design for the platform on which they appear. The menus are shown here as they appear on the Macintosh.

Extending the menu bar

The menu bar for a Program instance is created by calling the factory method createMenuBar, which has the following definition in the Program class:

 

protected ProgramMenuBar createMenuBar() {
   return new ProgramMenuBar();
}

In order to create a different menu bar, the application programmer can override this method so that it returns a different ProgramMenuBar, which will typically be an instance of a programmer-defined subclass of the base class. For example, a specific application program could redefine the factory method as

 

protected ProgramMenuBar createMenuBar() {
   return new MySpecialMenuBar();
}

With this definition, the program would create its menu bar by constructing an instance of MySpecialMenuBar instead.

The ProgramMenuBar class is a subclass of the standard Swing JMenuBar class. You can add JMenus and JMenuItems to it, just as you would assemble any menu bar. The standard initialization code for the ProgramMenuBar class is shown in Figure 6-11. Subclasses can override any of these methods to change the structure of the menu bar and the menus and items it contains. The standard menu items defined by the Program class can be included in user-defined menus by calling the createStandardMenuItem method as shown in the default implementation.

Figure 6-11. Standard code to initialize a ProgramMenuBar
/**
 * Initializes the menu bar.  Subclasses can override this method to
 * change the set of menus.
 */
   public void init() {
      addFileMenu();
      addEditMenu();
   }
 
/**
 * Installs the File menu.
 */
   public void addFileMenu() {
      JMenu fileMenu = new JMenu("File");
      fileMenu.setMnemonic('F');
      addFileMenuItems(fileMenu);
      add(fileMenu);
   }
 
/**
 * Installs the Edit menu.
 */
   public void addEditMenu() {
      JMenu editMenu = new JMenu("Edit");
      editMenu.setMnemonic('E');
      addEditMenuItems(editMenu);
      add(editMenu);
   }
 
/**
 * Adds the standard File items to the specified menu.  Subclasses
 * can override this method to change the list of items.
 */
   public void addFileMenuItems(JMenu menu) {
      menu.add(createStandardMenuItem("Save"));
      menu.add(createStandardMenuItem("Save As"));
      menu.addSeparator();
      menu.add(createStandardMenuItem("Print"));
      menu.add(createStandardMenuItem("Print Console"));
      menu.add(createStandardMenuItem("Script"));
      menu.addSeparator();
      menu.add(createStandardMenuItem("Quit"));
   }
 
/**
 * Adds the standard Edit items to the specified menu.  Subclasses
 * can override this method to change the list of items.
 */
   public void addEditMenuItems(JMenu menu) {
      menu.add(createStandardMenuItem("Cut"));
      menu.add(createStandardMenuItem("Copy"));
      menu.add(createStandardMenuItem("Paste"));
      menu.add(createStandardMenuItem("Select All"));
   }
} 

As an example, suppose that you were developing a program called SpellCheckingEditor and wanted to include a Spell Check item in the Edit menu. To do so, you could override the addEditMenuItems method like this:

 

public void addEditMenuItems(JMenu menu) {
   super.addEditMenuItems(menu);
   menu.addSeparator();
   JMenuItem spellCheckItem = new JMenuItem("Spell Check");
   spellCheckItem.addActionListener(new ActionListener {
      public void actionPerformed(ActionEvent e) {
         ((SpellCheckingEditor) getProgram()).spellCheck();
      }
   });
   menu.add(spellCheckItem);
}

In addition to illustrating how items can be added to a menu, this code also shows how the programmer can specify the response to the menu item. As in the standard menu-handling paradigm in Swing, the strategy is to add an action listener to the menu item. When the menu item is activated, the associated action listener is sent an actionPerformed method. In this case, actionPerformed initiates a call to the spellCheck method in the SpellCheckingEditor program that controls the menu bar.

Implementation issues with respect to menu bars

Menu bars proved to be one of the most difficult aspect of the acm.program package to get right. In many ways, the overarching design criterion for menu bars is that they should behave so intuitively that the user does not have to think about them at all. Unfortunately, achieving this goal means that menu bars must be implemented in a system-dependent way because each platform has a particular notion of what constitutes intuitive behavior. On Microsoft Windows and most flavors of Unix that support window systems, the menu bars are included in the program frame. On the Apple Macintosh, the menu bar appears at the top of the entire screen. In that context, having two menu bars—one for the window and one for the application—only generates confusion. When a program is running as an applet, the appropriate structure for a menu bar depends on the capabilities of the browser.

At the same time, it is essential to shield that platform dependence from programmers who are coding at the novice level. The strategy that we eventually chose hides that complexity in the code for the ProgramMenuBar class. On most systems for which a JMenuBar is the appropriate structure, the Program class simply installs the menu bar in the appropriate place in the JFrame used in the application. On the Macintosh, the Program class installs an AWT MenuBar instead but ensures that the effect of that MenuBar is what the application designer expects. Each MenuItem in the AWT MenuBar listens to the corresponding JMenuItem in the ProgramMenuBar and changes when that item changes. Thus, if the programmer enables or disables a menu item in the ProgramMenuBar, that change will be reflected in the Macintosh menus as well.

6.7 Other features of the Program class

The Program class includes a few important features beyond those outlined in the preceding sections. Although each of these features was mentioned briefly at the end of the introduction to this chapter, it is useful to describe them in more detail.

Automatic identification of the main class

One of the most important simplifications offered by the Program class is the elimination of the static main method that has been cited by so many instructors as a source of difficulty. As noted in section 6.4, the Program class includes its own public static void main method, which students therefore do not have to write. The implementation of the main method in the Program class executes the following steps:

  1. Determines the identity of the actual class that the user intended to run.
  2. Instantiates a new instance of that class by calling its default constructor.
  3. Creates a new JFrame for the program if its content pane has any components.
  4. Resizes the frame as described in the following section.
  5. Invokes the init method, as in the Applet/JApplet paradigm.
  6. Invokes validate to ensure that components are correctly sized in the frame.
  7. Invokes the run method in the Program object being executed.

At the time of the February 2005 release of this rationale document, the Task Force believed that it was not possible to implement this strategy on all of the major platforms. The only problematic step is the first: determining the identity of the class that the user intended to run, which we will identify as the main class. While this problem might seem straightforward, the current design of Java provides no platform-independent mechanism to do so. Fortunately, there are platform-specific mechanisms that work for Microsoft Windows, Mac OS X, and Linux, which are the primary platforms we expect adopters to use. The current implementation of the Program class supports each of these platforms.

If you are using a platform not included in this list or are working with an Integrated Development Environments (IDE) that has not been updated to support the Java Task Force packages, you may have to adopt a slightly more cumbersome strategy. Even before attempting its platform-dependent algorithm to identify the main class, the Program initialization code parses the arguments passed to main. Any parameter that has the form

 
name=value

is interpreted as a parameter definition and made available to the program through the getParameter method defined by the Applet class. The parameters that appear in the HTML APPLET tag are treated specially at startup time and can be used to designate not only the main class but other such useful parameters as the width and height of the window. These parameters can be passed explicitly on the command line like this:

 

> java -cp .:acm.jar Hello code=Hello.class width=300 height=200

This strategy can also be used in IDEs that make it possible to pass command-line arguments to the program being invoked. In those environments, the instructor can provide students with assignment templates that include the code parameter.

If the strategy of passing parameters does not work, it is always possible to add an explicit public static void main method to the Program code. The code is entirely canonical and looks like this:

 

public static void main(String[] args) {
   new MainClass().start(args);
}

where MainClass is the name of the main class. Including this method ensures maximum portability but significantly increases the conceptual overhead necessary for students to write their first programs.

We have also taken steps to ensure that this problem can eventually be solved in a much cleaner way. Together with Cay Horstmann, we submitted a request to the Java Community Process to include a new property called java.main that can be obtained through System.getProperty. The current implementation of the Program class already looks for this property. When it is implemented, a great deal of unnecessary code can be eliminated.

Setting the size and location of the application window

In an applet, the size of the window is determined by the width and height parameters to the applet tag in an HTML file. Applications, however, have no direct counterpart. The traditional strategy for building applications requires the programmer to set the bounds explicitly with a setSize or setBounds call applied to the Frame or JFrame object that encloses the application. When one is using the acm.program package, setting up that frame is under the control of the Program startup code, which suggests that a different approach is necessary to setting size and location.

The Program startup code chooses the size and location of the frame by applying the following strategies in order:

  1. Look for command-line parameter definitions with the names x, y, width, and height. If these parameters are supplied, the integer value that follows is used. Thus, if you invoke an application using

     
    
    java -cp .:acm.jar Hello code=Hello.class width=300 height=200
    
    the width and height of the application frame will be set to 300 and 200, respectively.

  2. If no command line parameters are defined for these values, look for static constant definitions of the following variables in the main class: APPLICATION_X, APPLICATION_Y, APPLICATION_WIDTH, and APPLICATION_HEIGHT. If any of these variables is defined, the program uses its value to set the corresponding parameter.
  3. If neither of these strategies succeeds, use default values for these parameters as specified in the Program class.

Applet capabilities

Because Program is an indirect subclass of Applet, programs have access to the methods provided by Java applets even when they run as applications. Of these capabilities, the most useful are the following:

  • Code base resources. Applets have the ability to read resources—typically images and audio clips—from their code or document base. When a program runs as an application, its code and document base are set to the current directory. Although these facilities do make it possible to load resources in the way that applets traditionally have, the acm.util package described in Chapter 8 includes a MediaTools class that offers a more robust set of tools that works in both the applet and application environments.

  • Simplified parameter access. Applets can be given parameters using the param tag in the HTML file. Programs running as applications automatically scan the command-line arguments and interpret any argument of the form

     
    
    name=value
    

    as a parameter definition.

  • Status display. The showStatus method in an applet is redefined so that programs running as applications print the status line on the standard output stream.

6.8 Strategies for using the Program class

One of the most important learning experience to come out of the Task Force was that different people teaching introductory computer science have widely divergent views about how to teach programming in an object-oriented language such as Java. Many instructors put off any significant coverage of objects until late in the introductory course, after the students have already learned to use the more traditional procedural paradigm. Even among those who believe that it is important to introduce objects early—the curriculum strategy that Computing Curricula 2001 [ACM01] called the objects-first approach—there are significant strategic and tactical differences as to how to do so.

The central point of contention is which aspects of object-oriented programming need to be brought out early, and which can be deferred. Some instructors, for example, believe that it is important to emphasize the notion of message passing and, in particular, to ensure that students are comfortable with the receiver-based syntax used in object-oriented languages to invoke a method on an object:

 

receiver.method(arguments)

For instructors who support this view, the specification of a receiver object is the key concept to learn early. Some instructors emphasize the role of the receiver by requiring students to specify this when an object makes calls to its own methods.

Another group of instructors holds that the critical concept to present early is the overall hierarchical structure of objects and classes. In this view, students should learn early the difference between objects and classes along with the idea that objects of one class are also objects of its various superclasses. This contingent tends to present examples of inheritance early, emphasizing the notion that subclasses extend the behavior of their superclasses. By contrast, instructors in the community that focuses on message-passing typically regard inheritance as less essential in the early part of the term.

Although strong opinions were expressed within the Task Force on both sides of this debate, it seems clear that each model has been used successfully to introduce object-oriented programming to students. It would not make sense for the Task Force to endorse one model over the other. Instead, it is important for the ACM packages to support each of these models well and enable students to use either style from the very beginning of the course. The following section offers several implementations of a flower-drawing program that illustrates each of these styles.

A simple graphical example: Drawing a flower using GTurtle

During the first weeks of an introductory programming course, many instructors introduce some sort of microworld, such as the LOGO system described by Seymour Papert in Mindstorms [Papert80] or Richard Pattis’s Karel the Robot [Pattis94]. Although the Task Force did not develop a complete microworld system, the graphical objects—particularly GTurtle—can fulfill much the same role. Figure 6-12, for example, shows one approach to writing a graphics program that uses GTurtle to draw a flower consisting of 36 squares with a 10-degree rotation between each one.

Figure 6-12. A single-method program that draws a turtle flower
/*
 * File: DrawTurtleFlower.java
 * --------------------------- 
 * This program draws a turtle flower using receiver-based
 * invocations in a single, undecomposed run method.
 */
 
import acm.graphics.*;
import acm.program.*;
 
public class DrawTurtleFlower extends GraphicsProgram {
 
/**
 * Runs the program.  This program creates a GTurtle object,
 * puts it in the center of the screen, and then draws a flower.
 * The flower consists of 36 squares, with a 10-degree rotation
 * between each one.  The squares, in turn, are drawn by drawing
 * four line segments interspersed with 90-degree rotations.
 */
   public void run() {
      GTurtle turtle = new GTurtle();
      add(turtle, getWidth() / 2, getHeight() / 2);
      turtle.penDown();
      for (int i = 0; i < 36; i++) {
         for (int j = 0; j < 4; j++) {
            turtle.forward(100);
            turtle.left(90);
         }
         turtle.left(10);
      }
   }
} 

Although this implementation illustrates the idea of sending a message to a GTurtle object and gives students practice with for loops, it takes no advantage of the power of decomposition, which is one of the fundamental programming principles that microworlds make so easy to illustrate. Unfortunately, breaking the run method down into components is not as simple as it might appear. The primary issue you need to resolve is how to share the GTurtle object with the subsidiary methods. Passing it as a parameter to each level has stylistic advantages but seems likely to add more complexity than novice programmers can easily assimilate. The other obvious approach is to declare turtle as an instance variable for the class, which gives rise to the code in Figure 6-13.

Figure 6-13. A first attempt at decomposing the DrawTurtleFlower program
/*
 * File: DrawTurtleFlower.java
 * --------------------------- 
 * This program draws a turtle flower using receiver-based
 * invocations and two levels of decomposition.
 */
 
import acm.graphics.*;
import acm.program.*;
 
public class DrawTurtleFlower extends GraphicsProgram {
 
/**
 * Runs the program.  This program creates a GTurtle object,
 * puts it in the center of the screen, and then draws a flower.
 */
   public void run() {
      turtle = new GTurtle();
      add(turtle, getWidth() / 2, getHeight() / 2);
      turtle.penDown();
      drawFlower();
   }
 
/** Draws a flower with 36 squares separated by 10-degree turns. */
   private void drawFlower() {
      for (int i = 0; i < 36; i++) {
         drawSquare();
         turtle.left(10);
      }
   }
 
/** Draws a square with four lines separated by 90-degree turns. */
   private void drawSquare() {
      for (int i = 0; i < 4; i++) {
         turtle.forward(100);
         turtle.left(90);
      }
   }
 
/** Holds the GTurtle object as an instance variable */
   private GTurtle turtle;
 
} 

The problem with this coding is that the structure seems unnatural to programmers who are predisposed to an early introduction of inheritance. Conceptually, this program defines a turtle. You tell the turtle to turn left by writing

 

turtle.left();

but draw a square by writing

 

drawSquare();

The asymmetry is confusing. Why isn’t drawSquare also a message to the turtle in the way that left is?

The answer, of course, is simply that the methods drawFlower and drawSquare are defined as methods in the DrawTurtleFlower class and not in the GTurtle object stored in the variable turtle. That object knows how to respond to the left message but not to the drawFlower message. It is, however, easy enough to reorganize the program so that the main program instantiates a new FlowerTurtle subclass that has these capabilities. That program appears in Figure 6-14. Note that the new coding no longer requires an instance variable and that it has a consistent calling structure. The DrawTurtleFlower class creates a new FlowerTurtle object and sends it the drawFlower message using the receiver-based syntax turtle.drawFlower();

Inside the definition of FlowerTurtle, however, all methods are invoked without an explicit receiver object, because they operate on the current instance of FlowerTurtle. Some instructors regard this coding as emblematic of good object-oriented design; others regard the absence of receivers as a sure indication that the program is not really object-oriented at all. The point is that the two coding styles are likely to appeal to instructors with different emphases, and it is important for the Java Task Force to support both constituencies.

Figure 6-14. A DrawTurtleFlower implementation using a FlowerTurtle subclass
/*
 * File: DrawTurtleFlower.java
 * --------------------------- 
 * This program draws a turtle flower using a GTurtle subclass
 * that knows how to draw flowers.
 */
 
import acm.graphics.*;
import acm.program.*;
 
public class DrawTurtleFlower extends GraphicsProgram {
 
/**
 * Runs the program.  This program creates a FlowerTurtle object,
 * centers it in the screen, and then asks it to draw a flower.
 */
   public void run() {
      FlowerTurtle turtle = new FlowerTurtle();
      add(turtle, getWidth() / 2, getHeight() / 2);
      turtle.penDown();
      turtle.drawFlower();
   }
}
 
/**
 * A GTurtle subclass that knows how to draw a flower.
 */
 
class FlowerTurtle extends GTurtle {
 
/** Draws a flower with 36 squares separated by 10-degree turns. */
   public void drawFlower() {
      for (int i = 0; i < 36; i++) {
         drawSquare();
         left(10);
      }
   }
 
/** Draws a square with four lines separated by 90-degree turns. */
   private void drawSquare() {
      for (int i = 0; i < 4; i++) {
         forward(100);
         left(90);
      }
   }
} 

Using GTurtle subclasses as standalone programs

Although the code in Figure 6-14 offers an example of how inheritance can be employed in a useful way, it may be beyond the level of what one can teach at the very beginning of an introductory course. If nothing else, the program contains two separate classes: the DrawTurtleFlower class that acts as the main program and FlowerTurtle that extends GTurtle so that it can draw a flower. It’s hard enough to get new students to understand a single class at the beginning of the course, and introducing two might send some over the edge.

To make this style of coding easier for students and teachers to use, the GTurtle class includes a special hook that allows it to operate as a standalone program. If a GTurtle or one of its subclasses is specified as the main class, its implementation of main starts up the program by performing the following operations:

  1. Instantiate an object of the desired GTurtle subclass using exactly the strategy described in the earlier subsection entitled “Automatic identification of the main class.”

  2. Instantiate a new GraphicsProgram object and installing it in the frame, just as if the main class were itself a graphics program and not a graphical object.

  3. Add the GTurtle object at the center of the canvas installed in the GraphicsProgram.

  4. Invoke the run method in the GTurtle subclass.

This interpretation provides a very simple framework for creating animated graphical programs in the first few days of the term, as shown in Figure 6-15.

Figure 6-15. Using the FlowerTurtle subclass as a standalone program
/*
 * File: FlowerTurtleProgram.java
 * ------------------------------ 
 * This program draws a turtle flower by invoking a GTurtle
 * object as if it were a program.
 */
 
import acm.graphics.*;
 
public class FlowerTurtleProgram extends GTurtle {
 
/** Runs the program. */
   public void run() {
      penDown();
      drawFlower();
   }
 
/** Draws a flower with 36 squares separated by 10-degree turns. */
   private void drawFlower() {
      for (int i = 0; i < 36; i++) {
         drawSquare();
         left(10);
      }
   }
 
/** Draws a square with four lines separated by 90-degree turns. */
   private void drawSquare() {
      for (int i = 0; i < 4; i++) {
         forward(100);
         left(90);
      }
   }
} 

6.9 The decision to use the Applet model

As the discussion in this chapter emphasizes, the Program class attempts to combine the best features of applications and applets and to make the facilities of both mechanisms available to students. Most development environments today support the application paradigm, but the ability to run programs as applets offers the obvious advantage of making it possible to run programs on the web. That capability makes it easier for the Java Task Force to create demonstration programs, but also holds forth the promise of enabling students to make their programs easily available to friends and family.

Despite these advantages, the decision to use the applet paradigm flies in the face of the direction that Java seems to be taking. Support for applets is eroding in the Java community, as evidenced by the fact that Sun’s Java tutorial no longer uses applets for its demos. In light of this decline in the fortunes of the applet paradigm, it is important for us to outline why we have chosen to embrace applets in the design of the acm.program package. The sections that follow recount the evolution of that decision and offer several reasons why we believe that this strategy will provide a stable approach to the problem of creating web-accessible programs.

The rise and fall of the applet paradigm

Ever since its introduction in 1995, educators have recognized that Java represents an attractive environment for the development of interactive teaching materials, primarily because of its integration with the web through the applet mechanism. The ITiCSE conferences in both 1997 and 1998 included working groups seeking to develop Java as a resource for creating visualizations and interactive animations for instructional use [Naps97, Bergin98]. The 1997 report offers the following assessment of the opportunities that Java provides:

Visualization has long been an important pedagogical tool in CS education. The widespread use of the Web and the introduction of Java, with its ability to present interactive animated applets and other types of animation, all provide opportunities to expand the availability of visualization-based teaching and learning. [Bergin98]

In recent years, however, the use of web-based resources in Java—and particularly those based on the applet paradigm—have declined in popularity as it became more difficult to maintain compatibility among applets running in an ever-expanding array of browsers that often implement radically different versions of the Java Development Kit (JDK). These compatibility problems have caused applets to fall out of favor in the Java community, where they have been replaced in many environments by plug-ins that implement the Java virtual machine or by the Java Web Start technology supported by Sun [Sun05]. That compatibility issues and the continuing evolution of the JDK are at the root of this change is clearly reflected in the following introductory passage from a 2001 article in Java World:

Java applets fueled Java’s initial growth. The ability to download code over the network and run it on a variety of desktops offering a rich user interaction proved quite compelling. However, Java’s Write Once, Run Anywhere (WORA) promise soon became strained as browsers began to bloat and several incompatibilities emerged that were caused by the Java language itself. [Srinivas01]

The strategy of using Java Web Start or browser-specific plugins does not represent a real solution to the problem of browser incompatibility. Java Web Start, for example, is not implemented on all platforms and, even when implemented, typically requires the user to download and install the appropriate plug-in. More importantly, Java Web Start applications run only if the version of the Java runtime on the user’s system is up to date with respect the web-based code. In a sense, the abandonment of the applet paradigm has not fostered any increase in compatibility but merely made it possible for both Sun Microsystems and the browser vendors to declare those incompatibilities to be, in effect, “someone else’s problem.” What has changed is that there is no longer an expectation that applets will work compatibly in different browsers, forcing the adoption of an alternate strategy.

Making applets work

The reality of the situation with regard to applets, however, is not quite as gloomy as the preceding section implies. Although it is impossible to ensure that any Java applet will run correctly in all browsers, it is possible to engineer specific applets so that they run acceptably well in most of them. The difficulty, however, lies in the fact that making applets run in a wide range of browsers forces one to adopt a lowest-common-denominator strategy that runs counter to the modern approaches to Java that one wants to offer to students.

To illustrate the severity of the problem, it is still the case that almost any browser you encounter in a commercial Internet-access storefront will be running the 1.1 version of the JDK. If you walk into your local Kinko’s outlet or the nearest easyInternetCafe, JDK 1.1 is all you find. Given that JDK 1.2 was released in December 1998, these browsers are more than eight years out of date, which is an eternity in the fast-paced world of computing technology. JDK 1.1 lacks support for Swing, for collections, and for many other tools that modern Java programmers cannot live without. The cost of living in that world seems to far outweigh any possible benefits.

It is, of course, quite legitimate to ask why anyone would care about old browsers such as those in Internet storefronts. That is not, presumably, where most students work, nor is it the place from which prospective adopters are most likely to look at the Java Task Force materials. Anyone who is prepared to teach Java must be willing to do so in a more modern Java environment, and it would be wrong to encourage them to do otherwise.

That view, however, may be too optimistic. While it is unlikely that students in a well-endowed university find themselves faced with obsolete technology, the same cannot be said for schools in low-income areas or institutions that focus on distance-learning and consequently have less control over the hardware and software environments their students use. In those environments, it is not at all unlikely that someone will boot up their machine, invoke the browser that comes for free on the desktop, and discover that it doesn’t run any of their programs. The failure mode associated with running a new applet in an old browser is also particularly bad from a pedagogical point of view. The applet simply fails to load, and the error message—if it is visible at all—tends to be entirely unhelpful.

But there is yet another consideration that militates in favor of allowing programs to run in older environments. One of the great things about allowing student programs to run as applets is that doing so permits them to share those programs easily with relatives and friends. Think for a moment about a student from the inner city, the first in the family to attend college, who takes an introductory computing course and completes an assignment that generates a great deal of pride. Such students should be encouraged to put their programs up on the web and to tell their parents about them. The parents may not have access to the web outside of a Kinko’s or a public library, and might very well be unable to obtain a better, more modern browser. If the student’s applet runs in that environment, the parents will be able to see it; if not, they are out of luck.

Evolution of the Java Task Force strategy

As noted in Chapter 1, the February 2005 release of the Java Task Force materials used only JDK 1.1 constructs in the implementation of the library itself, thereby making it possible for applets built using the libraries to run in the most commonly available browsers. This approach generated considerable criticism. Much of that criticism reflected the mistaken belief that this decision in some way constrained adopters to adhere to the same restrictions. In fact, the initial implementation of the packages did take advantage of JDK 1.2 features if they were available. If they were not, the packages tried to do something reasonable to ensure that the programs would run on old browsers that did not support those features.

At its meeting in June 2005, the Task Force decide that it had to abandon that position and adopt a more modern Java baseline for its own implementation. After some deliberation, we chose JDK 1.4 as the appropriate compromise in that it supports more of Java than our earlier design but does not insist on a version that is still unsupported on many common platforms. Our reasons for making that decision were as follows:

  • The introduction of the acm.gui package essentially forced us to use some Swing features. Earlier implementations of the JTF packages did not need to refer to Swing classes because they worked in what was largely an orthogonal domain. The GUI components, however, were firmly rooted in the Swing model.
  • It was impossible to use the code for the library packages as a model if we continued to adhere to the JDK 1.1 rule. Although the code used no deprecated features, it also did not take advantage of newer features that we would like students to use. Moreover, to the extent that the code attempted to use more advanced features, it did so through reflection, thereby rendering the code much more difficult to follow.
  • We discovered through experience that it was nearly impossible to dispel the misconceptions about the reasons for adopting the JDK 1.1 strategy. Too many people decided that our implementation decision inevitably meant that the JTF packages were obsolete. Although we could convince most detractors otherwise if we had the opportunity to explain our position on the issue, there was nothing we could do about potential adopters who chose to reject the JTF packages based on a misconception that we had no opportunity to correct.

At the same time, we did not wish to abandon the goal of allowing the JTF packages to function in the widest possible set of browsers. In particular, we wanted to code the libraries using JDK 1.4 but somehow have it run in JDK 1.1 browsers. At first glance, this goal sounds impossible. One possible approach that might have succeeded was to develop two versions of the acm.jar library, one for use with JDK 1.4, and one for use with JDK 1.1 to provide legacy browser support. That strategy, however, requires maintaining two separate versions of the library code, which would represent a maintenance nightmare. We regarded that strategy as unacceptable and did not pursue it.

The best of both worlds

After giving up on the idea of supporting legacy browsers on several occasions, we eventually hit upon a strategy that provides exceptional browser compatibility while allowing a modern coding style. Although it at first seems difficult to imagine, the strategy offers all of the following advantages:

  1. The library packages themselves are written in a pure JDK 1.4 style without special hooks to ensure backward compatibility. As a result, the library packages can serve as a model for other developers.
  2. The code can be compiled in a way that allows it to run in JDK 1.1 browsers.
  3. Only one version of the library code exists.

The key to making this strategy work lies in providing additional classes that implement simplified versions of the JDK 1.4 classes on which the library depends. If the program and libraries are compiled with these additional classes, the resulting code runs on JDK 1.1 browsers. If the programs and libraries are compiled alone, the program relies instead on the system implementation of these classes. Operationally, students can include the compatibility code by compiling their programs with the acm11.jar file instead of the standard acm.jar. Because the acm11.jar file includes the additional classes designed to ensure backward compatibility, the resulting program runs in JDK 1.1 environments.

The essential character of this strategy is easiest to illustrate by example. Suppose that a student wants to include a button in an application. In any modern version of Java, that button will be implemented using Swing’s JButton class. Unfortunately, JDK 1.1 browsers don’t know about the JButton class, which was introduced in JDK 1.2. It is hardly appropriate to suggest that students use the older Button class instead, because doing so would require them to use a Java paradigm that has long been obsolete. On the other hand, it is perfectly acceptable for students to write their code using JButton but to provide that class in the acm11.jar file. The student’s code is written precisely as it should be, but compatibility can nonetheless be maintained by simulating the JButton class in terms of more primitive AWT features.

The acm11.jar file contains implementations for every class that is defined in Chapter 9 as part of the JTF subset but which is not already part of JDK 1.1. These classes include the collection classes in the java.util package and the most common interactors defined in javax.swing. It does not, however, need to implement those features of Swing that lie outside the JTF subset. The resulting simplification turns out to be enormous. The code to accomplish the goal of simulating the new classes in the JTF subset, while not at all trivial, is surprisingly small. Given that the subset includes only a very small fraction of the classes involved in the Swing domain, the basic functionality of those classes can be simulated using less than two percent of the source code required for the full implementation of Swing.

In point of fact, this strategy is not quite as easy to implement as it sounds. The new classes defined as part of the acm11.jar file cannot actually live in the java and javax package hierarchy because the security manager for applets ordinarily makes it illegal to load user classes into those packages. (Being able to define new classes in system packages would indeed represent a significant security loophole in that such classes would have access to the protected information for that package.) Thus, the new classes provided by acm11.jar—such as the backward-compatible implementation of JButton—have to live somewhere else. At first glance, that restriction suggests that the user’s code would need to include additional import statements to gain access to those classes, which would violate the principle that there be only one version of the source. In actuality, the problem is even worse than that. If the acm11.jar file did assign the compatibility classes to a new package, the references to those classes would typically ambiguous, given that a JButton class, for example, would then exist in both javax.swing and the package containing the compatibility classes.

There is, however, a simple stratagem that avoids both the ambiguity and the need for additional import statements. If the compatibility classes are defined in the same package as the one in which the reference occurs, the compiler will use those definitions before looking at the imports. The acm11.jar library therefore defines the set of classes required for compatibility as part of the unnamed package typically used in introductory courses. If a student program is compiled using the acm.jar library, any references to JButton will be resolved to the javax.swing package. If the program is instead compiled with acm11.jar, references to JButton will be resolved within the unnamed package. The acm11.jar uses a similar strategy to ensure that the code for the ACM packages themselves have access to the full set of classes in the recommended JTF subset. If one of the ACM packages uses a class that is not available in JDK 1.1, the acm11.jar defines a class with that name as part of the appropriate ACM package to ensure that the code will run on any JDK 1.1 browser.



Final Report—July 31, 2006