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


Events: An Overview

 

First, what is an event?

 

Where do you find classes related to events?

 

Classes related to events:

 

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

Let us review this example once more:

 

What really happens when a button is pressed?

 

How to work with Swing events without losing your mind:

 

Some general information about "event" classes:

 

Next, a tour of some "listeners":

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

Note:

 


Nested classes

 

It is possible to define classes inside top-level classes. One motivation for introducing this syntax was to improve event-handling.

There are four types of nested classes:

 


Using a local class to implement a listener

 

We will use a somewhat contrived (but simple!) example to illustrate:

Here is what we want the frame to look like:

Here is the code: (source file)

import java.awt.*;
import javax.swing.*;
import java.awt.event.*; 

class NewFrame extends JFrame 
    implements ActionListener {

    // Data. 
    JButton quitB;              // Quit button. 
    JButton helloB, worldB;     // Two silly buttons. 

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Two button example");
        this.setResizable (true);
        this.setSize (width, height);

        // We'll use a flowlayout 
        Container cPane = this.getContentPane();
        cPane.setLayout (new FlowLayout());

        // Quit button 
        quitB = new JButton ("Quit");
        quitB.setBackground (Color.red);
        quitB.addActionListener (this);
        cPane.add (quitB);

        // "Hello" button 
        helloB = new JButton ("Hello");
        helloB.setBackground (Color.green);
        helloB.addActionListener (this);
        cPane.add (helloB);

        // "World" button 
        worldB = new JButton ("World");
        worldB.setBackground (Color.green);
        worldB.addActionListener (this);
        cPane.add (worldB);

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

    // Need this method for 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 ("Hello")) {
            System.out.print ("Hello ");
        }
        else if (s.equalsIgnoreCase ("World")) {
            System.out.println ("World!");
        }
    }

} // End of class "NewFrame" 


public class TwoButton {

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

}
  

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 improper 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 the ActionListener of the quit button.
  • 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 a local class for one of the button-listeners: (source file)
  public NewFrame (int width, int height)
  {
      // Set the title and other frame parameters. 
      this.setTitle ("Two button example");
      this.setResizable (true);
      this.setSize (width, height);

      // We'll use a flowlayout 
      Container cPane = this.getContentPane();
      cPane.setLayout (new FlowLayout());

      // Quit button 
      quitB = new JButton ("Quit");
      quitB.setBackground (Color.red);

      // Local class definition INSIDE a method. 
      class QuitButtonListener implements ActionListener {
          public void actionPerformed (ActionEvent a)
          {
              System.exit (0);
          }
      }
      
      // Instantiate local class. 
      QuitButtonListener qListener = new QuitButtonListener();
      // Add the listener to the button. 
      quitB.addActionListener (qListener);
      // Finally, add the button to the frame. 
      cPane.add (quitB);

      // The rest is the same as before ... 
    }
  

Note:

  • Observe the scope of the class QuitActionListener:
    class NewFrame extends JFrame implements ActionListener {
    
        // ... 
    
        // 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:
        // Instantiate local class. 
        QuitActionListener qListener = new QuitActionListener();
        // Add the listener to the button. 
        quitB.addActionListener (qListener);
        
  • We can shorten this code:
        quitb.addActionListener (new QuitActionListener());
        
 

Next, we will make some modifications:

  • We will use local classes for each button.
  • We will use variables to hold the output strings, to highlight an important feature.
 

Here is the code: (source file)

class NewFrame extends JFrame {
    // Note: Frame does not implement ActionListener anymore

    // Data.
    JButton quitB;              // Quit button.
    JButton helloB, worldB;     // Two silly buttons.

    String helloStr = "Hello ";
    String worldStr = "World!";

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Two button example");
        this.setResizable (true);
        this.setSize (width, height);

        // We'll use a flowlayout 
        Container cPane = this.getContentPane();
        cPane.setLayout (new FlowLayout());

        // Quit button 
        quitB = new JButton ("Quit");
        quitB.setBackground (Color.red);

        // Local listener: 
        class QuitActionListener implements ActionListener {
            public void actionPerformed (ActionEvent a)
            {
                System.exit (0);
            }
        }
      
        quitB.addActionListener (new QuitActionListener());
        cPane.add (quitB);

        // "Hello" button 
        helloB = new JButton ("Hello");
        helloB.setBackground (Color.green);

        // Listener: 
        class HelloActionListener implements ActionListener {
            public void actionPerformed (ActionEvent a)
            {
                // Note: we are accessing top-level variable. 
                System.out.print (helloStr);
            }
        }
    
        helloB.addActionListener (new HelloActionListener());
        cPane.add (helloB);

        // "World" button: code is similar ... 
    }

} // End of class "NewFrame"
 
  • Observe how a top-level variable like helloStr can be accessed inside the inner class:
    class NewFrame extends JFrame {
    
        // ... 
    
        String helloStr = "Hello ";
        String worldStr = "World!";
    
        public NewFrame (int width, int height)
        {
            // ... 
    
            class HelloActionListener implements ActionListener {
                public void actionPerformed (ActionEvent a)
                {
                    // Note: we are accessing top-level variable. 
                    System.out.print (helloStr);
                }
            }
    
            // ... 
      }
        
    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.
 


Using a pre-defined adapter class to implement a listener

 

Next, we will continue our contrived example by printing something to System.out when a mouse-click occurs.

Again, we will use a local class: (source file)

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

      // We'll use a flowlayout 
      Container cPane = this.getContentPane();
      cPane.setLayout (new FlowLayout());

      // Quit button 
      quitB = new JButton ("Quit");
      quitB.setBackground (Color.red);

      // Local listener: 
      class QuitActionListener implements ActionListener {
          public void actionPerformed (ActionEvent a)
          {
              System.exit (0);
          }
      }
      
      quitB.addActionListener (new QuitActionListener());
      cPane.add (quitB);

      // "Hello" button 
      helloB = new JButton ("Hello");
      helloB.setBackground (Color.green);

      // Listener: 
      class HelloActionListener implements ActionListener {
          public void actionPerformed (ActionEvent a)
          {
              // Note: we are accessing a top-level variable. 
              System.out.print (helloStr);
          }
      }
    
      helloB.addActionListener (new HelloActionListener());
      cPane.add (helloB);

      // "World" button 
      worldB = new JButton ("World");
      worldB.setBackground (Color.green);

      // Listener: 
      class WorldActionListener implements ActionListener {
          public void actionPerformed (ActionEvent a)
          {
              System.out.print (worldStr);
          }
      }
    
      worldB.addActionListener (new WorldActionListener());
      cPane.add (worldB);

      // Deal with mouse clicks. 
      class FrameMouseListener implements MouseListener {

          public void mouseClicked (MouseEvent m)
          {
              System.out.println ("Mouse click!");
          }

  	  // 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) {}
      }
    
      // Add to frame. 
      cPane.addMouseListener (new FrameMouseListener());

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

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

      class FrameMouseListener 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) {}
      }
  

Java often 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)
        // Deal with mouse clicks. 
          class FrameMouseListener extends MouseAdapter {
    
              public void mouseClicked (MouseEvent m)
              {
                  System.out.println ("Mouse click!");
              }
    
      	  // Don't need these: 
              // public void mouseEntered (MouseEvent m) {} 
              // public void mouseExited (MouseEvent m) {} 
              // public void mousePressed (MouseEvent m) {} 
              // public void mouseReleased (MouseEvent m) {} 
          }
        
  • 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 JFrame).
 


Using an anonymous class to implement a listener

 

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

class NewFrame extends JFrame {

    // ... 

    // 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 JFrame {

    // ... 

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

        // Quit button 
        quitB = new JButton ("Quit");
        quitB.setBackground (Color.red);

        // Create an implementation right in the method call: 
        quitB.addActionListener (
            new ActionListener() {
                public void actionPerformed (ActionEvent a)
                {
                    System.exit (0);
                }
            }
        );

        cPane.add (quitB);

       // ... 

    }

} // End of class "NewFrame" 
  

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)
        // Deal with mouse clicks. 
        cPane.addMouseListener (
            new MouseAdapter () {
                public void mouseClicked (MouseEvent m)
                {
                    System.out.println ("Mouse click!");
                }
            }
        );
        

Exercise 10.1: 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 getEnumeration ()
        {
            // ... 
        }
    
    } // End of class "LinkedList" 
        
In this exercise, change the above code so that LinkedList does not implement Enumeration. Instead, in the method getEnumeration, 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.
NOTE: Both of these have behavior and syntax 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 library 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.



© 1998, Rahul Simha (revised 2017)