Session 24: Inner classes

Inner classes (Section 12.4)
    The problem
    The solution
Case study: Dictionary
Exercise

Inner classes

The problem

Textbook: Section 12.4

When we wrote our little incrementing program earlier, something peculiar happened: We defined actionPerformed() as a public instance method of ListeningExample, even though we don't really think of actionPerformed() as being something you might do to a ListeningExample. That is, it's not as if a main() method might create a ListeningExample and then call actionPerformed() on it.

You might declare a separate listening class for each button. For example, we might define the following.

class QuitListener implements ActionListener {
    public void actionPerformed(ActionEvent evt) {
        System.exit(0); // this makes entire program exit
    }
}
And then, in the ListeningExample constructor method, we would write
        quit_button.addActionListener(new QuitListener());
That works, and it solves the problem of keeping actionPerformed() out of the ListeningExample class.

That is, it works until we look at writing a listener for the increment button, and we see that it needs to know about the text field. We can get around this by making the IncrListener class remember the text field also: We'll pass the text field into the constructor method, and it will copy it into an instance variable.

class IncrListener implements ActionListener {
    private JTextField number_field;

    public IncrListener(JTextField number_field) {
        this.number_field = number_field;
    }

    public void actionPerformed(ActionEvent evt) {
        int i = Integer.parseInt(number_field.getText());
        number_field.setText("" + (i + 1));
    }
}

Once we have these two classes defined, we can write our ExternalListeningGUI class.


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

class QuitListener implements ActionListener // and so on, as above

class IncrListener implements ActionListener // and so on, as above

public class ExternalListeningGUI extends JFrame {
    JButton incr_button;
    JButton quit_button;
    JTextField number_field;

    public ExternalListeningGUI() {
        incr_button = new JButton("Increment");
        quit_button = new JButton("Quit");
        number_field = new JTextField();

        incr_button.addActionListener(new IncrListener(number_field));
        quit_button.addActionListener(new QuitListener());

        Container contents = getContentPane();
        contents.add(incr_button, BorderLayout.NORTH);
        contents.add(number_field, BorderLayout.CENTER);
        contents.add(quit_button, BorderLayout.SOUTH);
        pack();
    }

    public static void main(String[] args) {
        (new ExternalListeningGUI()).show();
    }
}

The solution

That approach is a bit of a pain, though: We had to go out of our way to make the IncrListener remember things about the instance at hand. Java actually provides an alternative approach, called the inner class. An inner class is a class definition that is nested within another class definition. In the instance methods of the outer class, you can create instances of the inner class. And the inner class will allow you to access instance members of the outer class instance.

It's confusing when described on paper. Basically, each instance of the inner class has an implicit pointer to an instance of the outer class. Whenever your code for the inner class accesses an instance member for the outer class, it goes through the pointer to get to the variable's value.

In practice, it's not so confusing. Here's how I would actually write the ListeningExample. Here, we've made QuitListener and IncrListener be inner classes.


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

public class InternalListeningGUI extends JFrame {
    private class QuitListener implements ActionListener {
        public void actionPerformed(ActionEvent evt) {
            System.exit(0); // this makes entire program exit
        }
    }

    private class IncrListener implements ActionListener {
        public void actionPerformed(ActionEvent evt) {
            int i = Integer.parseInt(number_field.getText());
            number_field.setText("" + (i + 1));
        }
    }

    private JButton incr_button;
    private JButton quit_button;
    private JTextField number_field;

    public InternalListeningGUI() {
        incr_button = new JButton("Increment");
        quit_button = new JButton("Quit");
        number_field = new JTextField();

        incr_button.addActionListener(new IncrListener());
        quit_button.addActionListener(new QuitListener());

        Container contents = getContentPane();
        contents.add(incr_button, BorderLayout.NORTH);
        contents.add(number_field, BorderLayout.CENTER);
        contents.add(quit_button, BorderLayout.SOUTH);
        pack();
    }

    public static void main(String[] args) {
        (new InternalListeningGUI()).show();
    }
}
A couple of things to notice about this:

Case study: Dictionary

To look at a more extended example using inner classes, we'll examine a Dictionary program.

The user can select a term from the menu, and the term's definition appears in the text area below.

 1  import java.awt.*;
 2  import java.awt.event.*;
 3  import javax.swing.*;
   
 4  public class Dictionary extends JFrame {
 5      // private class definitions
 6      private class TermMenuItem extends JMenuItem
 7              implements ActionListener {
 8          private String defn;
   
 9          public TermMenuItem(String term, String defn) {
10              super(term);
11              addActionListener(this);
12              this.defn = defn;
13          }
   
14          public void actionPerformed(ActionEvent evt) {
15              text_area.setText(defn);
16          }
17      }
   
18      private class WindowCloser extends WindowAdapter {
19          public void windowClosing(WindowEvent evt) {
20              System.exit(0);
21          }
22      }
   
23      // private instance variables
24      private JTextArea text_area;
   
25      // public instance methods
26      public Dictionary() {
27          super("Dictionary");
28          addWindowListener(new WindowCloser());
   
29          // set up menus
30          JMenuBar menubar = new JMenuBar();
31          JMenu menu = new JMenu("Term");
32          setJMenuBar(menubar);
33          menubar.add(menu);
34          menu.add(new TermMenuItem("JButton",
35              "A labeled button that can be pressed"));
36          menu.add(new TermMenuItem("JCheckbox",
37              "A box that can be clicked on or off"));
38          menu.add(new TermMenuItem("JLabel",
39              "A component containing only a string"));
40          menu.add(new TermMenuItem("JTextArea",
41              "A multiline area where text can be displayed or edited"));
   
42          // set up rest of frame
43          text_area = new JTextArea(5, 30);
44          getContentPane().add(text_area);
45          pack();
46      }
   
47      // public class methods
48      public static void main(String[] args) {
49          (new Dictionary()).show();
50      }
51  }

Lines 18-22: The WindowAdapter class

We saw that the WindowListener interface (used in our TemperatureConversion program) obligates a class to define many methods. Usually the program only wants the bodies of most of them to be empty. The libraries defines the WindowAdapter to deal with this.

The WindowAdapter class defines all of the methods required by WindowListener as empty methods. This enables a program to extend the WindowAdapter class, overriding only those methods that are important to the program. This is what is occurring in lines 18-22: We extend the WindowAdapter class (which implements the WindowListener interface) and override the windowClosing method to in fact close the program. Then in line 28, a new instance of this class is created and registered as a window listener for the Dictionary frame.

Line 24: The JTextArea class

The JTextArea class implements a blank area in which multiple lines of data can be displayed or entered. It works similarly to JTextField.

JTextArea(int rows, int columns)
Creates a text area to hold rows lines, each with columns characters in it.

String getText()
Returns the text currently in the text area.
void setText(String contents)
Changes the text currently in the text area to contents.
In this program, we create the text area in line 43. Line 15 (executed whenever the user chooses a new term) alters the text displayed within the text area.

Lines 30-41: The JMenuBar, JMenu, and JMenuItem classes

Line 30 creates a JMenuBar and line 32 tells the JFrame to use it as the window's menu bar. The JMenuBar is a container suitable for holding JMenus.

Line 31 creates a JMenu (titled ``Term'') and line 33 adds it into the menu bar. A JMenu is a component which is also a container suitable for holding JMenuItems. When the user clicks on a JMenu, the menu showing the menu items pops up.

Lines 34-41 create the JMenuItems (note how the TermMenuItem class extends JMenuItem) and insert them into the JMenu created in line 31. A JMenuItem is a lot like a JButton: The constructor method takes a string, the label of the menu item. And the addActionListener() method associates an action with what should happen when the user clicks on the menu item.

Notice that, in this case, an action listener is associated with the menu item in the subclass's constructor method in line 11.

Exercise