Module 10: Java Objects, Part IV: Inner Classes and Their Use in Handling AWT Events


AWT Events: An Overview

First, what is an event?

Classes related events in AWT:

We have already seen, for example, that a Button can generate an ActionEvent that must be handled by an ActionListener instance.

Let us review this example once more:

What really happens when the button is pressed?

An overview of some important events in AWT:

Other, less frequently used events include: ComponentEvent and ContainerEvent.

Next, a tour of some "listeners":

Other, less frequently used listeners include: ComponentListener and ContainerListener.

Note:


Nested classes

Java 1.1 introduced new syntax to allow programmers to define classes inside top-level classes. One motivation was to improve event-handling.

There are four types of nested classes:


Using a local class to implement a listener

We will use an example with two canvases to illustrate:

Here is what we want the frame to look like (after a few mouse-clicks in each canvas):
Window

Here is the code: (source file)


import java.awt.*;
import java.awt.event.*; // Needed for ActionListener. 

class NewFrame extends Frame 
  implements ActionListener, MouseListener {

  // Data. 
  Button quitb;               // Quit button. 
  Button clearb1, clearb2;    // Two clear buttons. 
  Canvas c1, c2;              // Two canvases.
  Panel p1, p2;               // One panel per canvas.
  Panel outerpanel;           // A panel to contain p1, p2.
  int 
    c1_current_x=0,           // Current mouse-click position
    c1_current_y=0,           // for canvas 1.
    c2_current_x=0, 
    c2_current_y=0;           // Same for canvas 2.

  // Constructor.
  public NewFrame (int width, int height)
  {
    // Set the title and other frame parameters.
    this.setTitle ("Two canvas example");
    this.setResizable (true);
    this.setBackground (Color.cyan);
    this.setSize (width, height);
    // this.setLayout (new BorderLayout());

    // Create a quit button for the whole frame.
    quitb = new Button ("Quit");
    quitb.setBackground (Color.red);
    quitb.setFont (new Font ("Serif", Font.PLAIN | Font.BOLD, 15));
    quitb.addActionListener (this);
    this.add (quitb, BorderLayout.SOUTH);

    // Create a Panel for the first canvas
    p1 = new Panel ();
    p1.setLayout (new BorderLayout());

    // Create a white canvas.
    c1 = new Canvas ();
    c1.setBackground (Color.white);
    c1.setForeground (Color.blue);
    c1.addMouseListener (this);

    // Add canvas to frame in the center.
    p1.add (c1, BorderLayout.CENTER);

    // Create a clear button.
    clearb1 = new Button ("Clear");
    clearb1.addActionListener (this);
    clearb1.setBackground (Color.green);
    p1.add (clearb1, BorderLayout.NORTH);

    // Create a Panel for the second canvas.
    p2 = new Panel ();
    p2.setLayout (new BorderLayout());

    // Create a second white canvas.
    c2 = new Canvas ();
    c2.setBackground (Color.white);
    c2.setForeground (Color.blue);
    c2.addMouseListener (this);

    // Add canvas to frame in the center.
    p2.add (c2, BorderLayout.CENTER);

    // Create a second clear button.
    clearb2 = new Button ("Clear");
    clearb2.addActionListener (this);
    clearb2.setBackground (Color.green);
    p2.add (clearb2, BorderLayout.NORTH);

    // A panel to hold the smaller panels.
    outerpanel = new Panel ();
    // Use 1-row, 2-column grid with 5-pixel gaps.
    outerpanel.setLayout (new GridLayout (1,2,5,5));
    outerpanel.add(p1);
    outerpanel.add(p2);

    // Now add the panel to the frame.
    this.add (outerpanel, BorderLayout.CENTER);

    // Show the frame.
    this.setVisible (true);
  }

  // This method is required to implement the 
  // ActionListener interface.
  public void actionPerformed (ActionEvent a)
  {
    // Get the button string.
    String s = a.getActionCommand();
    if (s.equalsIgnoreCase ("Quit"))
      System.exit(0);
    else if (s.equalsIgnoreCase ("Clear")) {
      // Need to find out which canvas:
      if (a.getSource() == clearb1) {
	c1.setBackground (Color.white);
	c1.repaint ();
	c1_current_x = c1_current_y = 0;
      }
      else {
	c2.setBackground (Color.white);
	c2.repaint ();
	c2_current_x = c2_current_y = 0;
      }
    }
  }

  // These methods are required to implement 
  // the MouseListener interface.
  public void mouseClicked (MouseEvent m)
  {
    int x = m.getX();
    int y = m.getY();
    // Need to figure out which canvas:
    if (m.getSource() == c1) {
      Graphics g = c1.getGraphics();
      g.drawLine (c1_current_x, c1_current_y, x, y);
      c1_current_x = x;
      c1_current_y = y;
    }
    else {
      Graphics g = c2.getGraphics();
      g.drawLine (c2_current_x, c2_current_y, x, y);
      c2_current_x = x;
      c2_current_y = y;
    }
  }

  // We need to implement these methods, but
  // don't actually have to do anything inside.
  public void mouseEntered (MouseEvent m) {}
  public void mouseExited (MouseEvent m) {}
  public void mousePressed (MouseEvent m) {}
  public void mouseReleased (MouseEvent m) {}

} // End of class "NewFrame"


public class TwoCanvas {

  public static void main (String[] argv)
  {
    NewFrame nf = new NewFrame (300, 200);
  }

}
  

Note:

  • Each canvas, along with its clear button, is placed in a panel.

  • The two panels are placed inside a larger panel (outerpanel).

  • The larger panel and the quit button are placed in the frame.

  • The frame implements the listener interfaces.

  • Since both clear buttons are called "Clear", we need some way of distinguishing between the two buttons:
    • The getSource() method returns the pointer to the component in which the event was generated.
    • This pointer can be compared to the pointer to the component:
      
        public void actionPerformed (ActionEvent a)
        {
          // ...
      
            if (a.getSource() == clearb1) {
      
          // ...
        }
         

The above approach of having the frame implement the listeners is undesirable for many reasons:

  • Logically, event handling for different components should be separate.
    (Instead of being in the same actionPerformed() method.)

  • As the number of components increase, the code complexity of such "all-in-one-place" event-handlers increases.

  • The frame should really be a frame and not an event listener.

  • The this pointer passed to a component's add-listener method can allow for inadvertent or malicious use of non-listener methods in the frame.

To solve this problem, Java provides the use of so-called local classes:

  • In the example below, we will use a local class for each ActionListener.
  • Since all the relevant code is in the constructor of NewFrame only the constructor will be shown.
  • Note the unusual syntax and the fact that a class is being defined in a body of code.
Here is an implementation using local classes for button-listeners: (source file)

  // Constructor.
  public NewFrame (int width, int height)
  {
    // Set the title and other frame parameters.
    this.setTitle ("Two canvas example");
    this.setResizable (true);
    this.setBackground (Color.cyan);
    this.setSize (width, height);
    // this.setLayout (new BorderLayout());

    // Create a quit button for the whole frame.
    quitb = new Button ("Quit");
    quitb.setBackground (Color.red);
    quitb.setFont (new Font ("Serif", Font.PLAIN | Font.BOLD, 15));

    // Create a local class. QuitActionListener is our name.
      class QuitActionListener implements ActionListener {
        public void actionPerformed (ActionEvent a) 
        {
	  System.exit(0);  // Action required for quit.
        }
      } 

    // Create an instance of the local class 
    // and pass it to the button.
    quitb.addActionListener (new QuitActionListener());

    // Now add the quit button to the frame.
    this.add (quitb, BorderLayout.SOUTH);

    // Create a Panel for the first canvas
    p1 = new Panel ();
    p1.setLayout (new BorderLayout());

    // Create a white canvas.
    c1 = new Canvas ();
    c1.setBackground (Color.white);
    c1.setForeground (Color.blue);
    c1.addMouseListener (this);

    // Add canvas to frame in the center.
    p1.add (c1, BorderLayout.CENTER);

    // Create a clear button.
    clearb1 = new Button ("Clear");
    clearb1.setBackground (Color.green);

    // First create a listener class.
      class ClearActionListener1 implements ActionListener {
        public void actionPerformed (ActionEvent a) 
        {
	  c1.setBackground (Color.white);
  	  c1.repaint ();
	  c1_current_x = c1_current_y = 0;
        }
      }

    // Pass an instance of the class to the button.
    clearb1.addActionListener (new ClearActionListener1());

    // Now add the button to the panel.
    p1.add (clearb1, BorderLayout.NORTH);


    // Create a Panel for the second canvas.
    p2 = new Panel ();
    p2.setLayout (new BorderLayout());

    // Create a second white canvas.
    c2 = new Canvas ();
    c2.setBackground (Color.white);
    c2.setForeground (Color.blue);
    c2.addMouseListener (this);

    // Add canvas to frame in the center.
    p2.add (c2, BorderLayout.CENTER);

    // Create a second clear button.
    clearb2 = new Button ("Clear");
    clearb2.setBackground (Color.green);

    // Define a local class for the second button
      class ClearActionListener2 implements ActionListener {
        public void actionPerformed (ActionEvent a) 
        {
	  c2.setBackground (Color.white);
	  c2.repaint ();
	  c2_current_x = c2_current_y = 0;
        }
      }

    // Pass an instance to the (second) clear button.
    clearb2.addActionListener (new ClearActionListener2());

    // Add the button to panel p2.
    p2.add (clearb2, BorderLayout.NORTH);

    // A panel to hold the smaller panels.
    outerpanel = new Panel ();
    outerpanel.setLayout (new GridLayout (1,2,5,5));
    outerpanel.add(p1);
    outerpanel.add(p2);

    // Now add the panel to the frame.
    this.add (outerpanel, BorderLayout.CENTER);

    // Show the frame.
    this.setVisible (true);
  }
  

Note:

  • Observe the scope of the class QuitActionListener:
    
    class NewFrame extends Frame implements MouseListener {
    
      // ...
    
      // Constructor.
      public NewFrame (int width, int height)
      {
        // ...
    
        // Create a local class. QuitActionListener is our name.
          class QuitActionListener implements ActionListener {
            public void actionPerformed (ActionEvent a) 
            {
    	  System.exit(0);  // Action required for quit.
            }
          } 
    
        // ...
      }
    
    }
        
  • Since the button must be passed an instance, we create one on the fly:
    
        // Create an instance of the local class 
        // and pass it to the button.
        quitb.addActionListener (new QuitActionListener());
        
  • Observe how a top-level variable like c1 or c1_current_x can be accessed inside the class:
    
          class ClearActionListener1 implements ActionListener {
            public void actionPerformed (ActionEvent a) 
            {
    	  c1.setBackground (Color.white);
    	  c1.repaint ();
    	  c1_current_x = c1_current_y = 0;
            }
          }
        
    Note:
    • Only top-level (heap) variables can be referenced in local classes, as above.
    • Method (local) variables and parameters that are on the stack cannot be referenced.


  • We did not use local classes for the mouse listener, and so the frame implements MouseListener.

Implementing the MouseListener interface in a local class is done similarly: (source file)


class NewFrame extends Frame {

  // ...

  // Constructor.
  public NewFrame (int width, int height)
  {
    // ...

    // Create a MouseListener class:
      class CanvasMouseListener1 implements MouseListener {
        public void mouseClicked (MouseEvent m)
        {
	  int x = m.getX();
  	  int y = m.getY();
	  Graphics g = c1.getGraphics();
	  g.drawLine (c1_current_x, c1_current_y, x, y);
	  c1_current_x = x;
	  c1_current_y = y;
        }

	// These methods need to be implemented 
	// to complete the interface.
        public void mouseEntered (MouseEvent m) {}
        public void mouseExited (MouseEvent m) {}
        public void mousePressed (MouseEvent m) {}
        public void mouseReleased (MouseEvent m) {}
      }

    // Pass an instance of the listener to canvas c1.
    c1.addMouseListener (new CanvasMouseListener1());

    // ...
  }

}
  

Note:

  • Since CanvasMouseListener1 implements the MouseListener interface, it must implement all methods in it.

  • For those that we don't care about, we provide an empty body.

We will improve the above code in the following ways:

  • Notice that the two clear-button listeners are almost identical.
    - we should be able to use two instances of the same class.

  • Similarly, the two mouse-listener classes should be rolled into one.

  • Instead of using c1_current_x and c1_current_y for representing a point, we will use use a Point instance.
    (Point is a class in AWT).

  • Again, we will only show the constructor of the frame, since that is where the new code is.

Here is the code: (source file)


class NewFrame extends Frame {

  // Data.
  Button quitb;
  Button clearb1, clearb2;
  Canvas c1, c2;
  Panel outerpanel, p1, p2;
  Point c1_current = new Point (0,0);  // Point instances.
  Point c2_current = new Point (0,0);

  // NewFrame Constructor.
  public NewFrame (int width, int height)
  {

    // ... 

    // Create a generic MouseListener class for both canvases.
      class CanvasMouseListener implements MouseListener {
	// Data.
        Canvas c;      // Store which canvas.
        Point current; // Store current click position.

	// Constructor.
        public CanvasMouseListener (Canvas c, Point current)
        {
	  this.c = c;   this.current = current;
        }

        // Handle a mouse click.
        public void mouseClicked (MouseEvent m)
        {
	  int x = m.getX();
	  int y = m.getY();
	  Graphics g = c.getGraphics();
	  g.drawLine (current.x, current.y, x, y);
	  current.x = x;
	  current.y = y;
        }

	// Empty methods - to complete interface.
        public void mouseEntered (MouseEvent m) {}
        public void mouseExited (MouseEvent m) {}
        public void mousePressed (MouseEvent m) {}
        public void mouseReleased (MouseEvent m) {}
      } // End of "CanvasMouseListener"

    // Create an instance, passing c1 stuff to the constructor.
    c1.addMouseListener (new CanvasMouseListener(c1, c1_current));

    // ...

    // Create a generic listener class for both clear buttons.
      class ClearActionListener implements ActionListener {
	// Data.
        Canvas c;        // Which canvas.
        Point current;   // The current point of that canvas.

	// Constructor.
        public ClearActionListener (Canvas c, Point current)
        {
	  this.c = c;   this.current = current;
        }

	// Handle the button-press.
        public void actionPerformed (ActionEvent a) 
        {
	  c.setBackground (Color.white);
	  c.repaint ();
	  current.x = current.y = 0;
        }
      } // End of "ClearActionListener"

    // Create an instance for canvas c1.
    clearb1.addActionListener (new ClearActionListener(c1, c1_current));

    // ... create the second canvas and clear button ...

    // Add a new instance of the MouseListener.
    c2.addMouseListener (new CanvasMouseListener(c2, c2_current));

    // Pass a new instance of the action listener.
    clearb2.addActionListener (new ClearActionListener(c2, c2_current));

    // ...

  }

  // ... 

}

  


Using a pre-defined adapter class to implement a listener

Recall that, even though we only wanted to handle mouseClicked we still had to provide (empty) implementations for other mouse methods like mouseDragged:


      class CanvasMouseListener implements MouseListener {

        // ...

        public void mouseClicked (MouseEvent m)
        {
          // ...
        }

	// Empty methods - to complete interface.
        public void mouseEntered (MouseEvent m) {}
        public void mouseExited (MouseEvent m) {}
        public void mousePressed (MouseEvent m) {}
        public void mouseReleased (MouseEvent m) {}
      }
  

The package java.awt.event provides special "adapter" classes to relieve this effort:

  • You don't really need to use these adapters if you don't want to.

  • The adapter classes simply provide empty implementations of all methods.

  • For example, MouseAdapter provides an empty implementation of all methods in MouseListener.

  • We will re-write our CanvasMouseListener by making it extend the MouseAdapter class: (source file)
    
        // Create a MouseListener class:
          class CanvasMouseListener extends MouseAdapter {
            // Data.
            Canvas c;
            Point current;
    
    	// Constructor.
            public CanvasMouseListener (Canvas c, Point current)
            {
    	  this.c = c;   this.current = current;
            }
    
    	// Handle a mouse click.
            public void mouseClicked (MouseEvent m)
            {
    	  int x = m.getX();
    	  int y = m.getY();
    	  Graphics g = c.getGraphics();
    	  g.drawLine (current.x, current.y, x, y);
    	  current.x = x;
    	  current.y = y;
            }
    
            // Don't need to implement these anymore.
            // public void mouseEntered (MouseEvent m) {}
            // public void mouseExited (MouseEvent m) {}
            // public void mousePressed (MouseEvent m) {}
            // public void mouseReleased (MouseEvent m) {}
          } // End of "CanvasMouseListener" 
        
  • Thus, one advantage of using local classes is that you can extend adapters as shown above.
    (We would not be able to do this with NewFrame since it already extends Frame).


Using an anonymous class to implement a listener

Recall the listener for the "quit" button defined earlier:


class NewFrame extends Frame {

  // ...

  // Constructor.
  public NewFrame (int width, int height)
  {

    // ...

    // Create a local class for the quit button.
      class QuitActionListener implements ActionListener {
        public void actionPerformed (ActionEvent a) 
        {
	  System.exit(0);
        }
      }

    // Pass an instance to the button.
    quitb.addActionListener (new QuitActionListener());

    // ...

  }

}
  

Here, we defined a class and used it only once, wasting a name.

Java provides the ability to define a class right where it's needed - in creating an instance: (source file)


class NewFrame extends Frame {

  // ...

  // Constructor.
  public NewFrame (int width, int height)
  {

    // ...

    // Using an anonymous class:
    quitb.addActionListener (
      new ActionListener() {
        public void actionPerformed (ActionEvent a) 
        {
	  System.exit(0);
        }
      }
    );

    // ...

  }

}
  

Note:

  • The class is defined in the parameter list of a method!

  • The syntax is a little strange.

  • You can think of it this way:
    
        quitb.addActionListener (
    
          // This is the parameter of addActionListener:
    
          new ActionListener() // A new instance of ActionListener.
    
          // This is the body of the class:
                               {
            public void actionPerformed (ActionEvent a) 
            {
    	  System.exit(0);
            }
          }
    
        ); // End of parameter list.
        
  • Stranger still is the fact that ActionListener is an interface.

  • Thus, the "anonymous" body above provides an implementation of the interface ActionListener.

  • You can extend an existing class with the same syntax, for example: (single canvas - source file)
    
        c.addMouseListener (
          new MouseAdapter () {
            public void mouseClicked (MouseEvent m)
    	{
    	  int x = m.getX();
    	  int y = m.getY();
    	  Graphics g = c.getGraphics();
    	  g.drawLine (current_x, current_y, x, y);
    	  current_x = x;
    	  current_y = y;
    	}
          }
        );
        

Exercise 10.1 (Solution): Recall that we implemented an Enumeration for linked lists by having the linked list class implement the Enumeration interface:

  • The definition of the Enumeration interface:
    
      public interface Enumeration {
        public abstract boolean hasMoreElements();
        public abstract Object nextElement();
      }
        
  • The class LinkedList itself implemented the Enumeration interface:
    
    class LinkedList implements Enumeration {
    
      // ... 
    
      public boolean hasMoreElements ()
      {
        // ...
      }
    
      public Object nextElement() 
      {
        // ...
      }
    
      public Enumeration get_enumeration ()
      {
        // ...
      }
    
    } // End of class "LinkedList"
        
In this exercise, change the above code so that LinkedList does not implement Enumeration. Instead, in the method get_enumeration, create an anonymous class that does the job right in the return statement.


Esoteric topics: static and instance member classes

For completeness, we will now consider the remaining two kinds of inner classes:

  • Static member classes.
  • Instance member classes.
Both of these have behavior that can be difficult to understand. If possible, avoid using them.

We will present the material via examples:

  • Static member classes:
    • Example: (source file)
      
      class A {
      
        // A static member class.
        static class B {
          int x;                // Data.
          public B (int i) {    // Constructor.
            x = i;
          }
          public void print ()  // A method.
          {
            System.out.println ("B: x=" + x);
          }
        }
      
      }
      
      public class TestStaticLocal {
       
        public static void main (String[] argv)
        {
          // Create an instance of B.
          A.B b = new A.B (1);
          b.print();            // Prints "1".
      
          // Create another instance of B.
          A.B b2 = new A.B (2);
          b2.print();           // Prints "2".
        }
      
      }
            
    • Note:
      • The class B is defined inside class A.
      • B is a static member of A.
      • The only reference to B is in the form A.B.
      • Thus, the only purpose is "packaging".
    • If you are building a large class and need to package together many related classes, this is one way of doing it.
    • That way, name-clashes can be avoided.
    • For example, if B is a really popular class name, then, by packaging it inside other classes, different B's can be differentiated.


  • Instance member classes:
    • In the above example, if the static keyword is removed, the code will not compile: (source file)
      
      class A {
      
        class B {
          int x;               // Data.
          public B (int i) {   // Constructor.
            x = i;
          }
          public void print () // A method.
          {
            System.out.println ("B: x=" + x);
          }
        }
      
      }
      
      public class TestLocal {
       
        public static void main (String[] argv)
        {
          A.B b = new A.B (1);  // Does not compile.
          b.print();
        }
      
      }
            
    • Here, we wanted to create an instance of B since B is not static anymore.
    • However, you need to create an instance of A before you can create an instance of B: (source file)
      
      class A {
      
        // Instance data in A
        int y;
      
        // Constructor for A.
        public A (int y)
        {
          this.y = y;
        }
      
        // Instance member class:
        class B {
          int x;                 // Data.
          public B (int i) {     // Constructor.
            x = i;
          }
          public void print ()   // A method.
          {
            System.out.println ("B: x=" + x + ", y=" + y);
          }
        }
      
      }
      
      public class TestLocal2 {
       
        public static void main (String[] argv)
        {
          // Create an instance of A first.
          A a = new A (1);      
      
          // Now create an "associated" instance of B.
          // Note the strange syntax!
          A.B b = a.new B (2);  
      
          b.print();      // Prints "B: x=2, y=1"
        }
      
      }
            
    • Such an instance member class is always associated with an instance of the containing class.
    • Instance member classes have strange syntax and behavior, and should be used with care.
    • This feature is useful when building a very large class. (Some AWT classes like Component are probably very large).
    • Inside a large class, you may want to re-use instances of of a smaller class. Then, to avoid name clashes, you can use the above construct.


  • Member classes can be nested to any depth, but such nesting is discouraged and ought to be used with extreme care if at all.