Java的内部类

Published on 2017 - 02 - 19

前言

An inner class is a class that is defined inside another class. Why would you want to do that? There are three reasons:

  • Inner class methods can access the data from the scope in which they are defined—including the data that would otherwise be private.
  • Inner classes can be hidden from other classes in the same package.
  • Anonymous inner classes are handy when you want to define callbacks without writing a lot of code.

Use of an Inner Class to Access Object State

public class TalkingClock
{
   private int interval;
   private boolean beep;
   public TalkingClock(int interval, boolean beep) { . . . }
   public void start() { . . . }

   public class TimePrinter implements ActionListener
      // an inner class
   {
      Date now = new Date();
      System.out.println("At the tone, the time is " + now);
      if (beep) Toolkit.getDefaultToolkit().beep();
   }
}

Note that the TimePrinter class is now located inside the TalkingClock class. This does not mean that every TalkingClock has a TimePrinter instance field.

Something surprising is going on. The TimePrinter class has no instance field or variable named beep. Instead, beep refers to the field of the TalkingClock object that created this TimePrinter. This is quite innovative. Traditionally, a method could refer to the data fields of the object invoking the method. An inner class method gets to access both its own data fields and those of the outer object creating it.

For this to work, an object of an inner class always gets an implicit reference to the object that created it.

The expression OuterClass.this denotes the outer class reference. For example, you can write the actionPerformed method of the TimePrinter inner class as

public void actionPerformed(ActionEvent event)
{
   . . .
   if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}

Conversely, you can write the inner object constructor more explicitly, using the syntax

outerObject.new InnerClass(construction parameters)

For example:

ActionListener listener = this.new TimePrinter();

Here, the outer class reference of the newly constructed TimePrinter object is set to the this reference of the method that creates the inner class object. since TimePrinter is a public inner class, you can construct a TimePrinter for any talking clock:

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

Inner classes are genuinely more powerful than regular classes because they have more access privileges.

You may well wonder how inner classes manage to acquire those added access privileges, if they are translated to regular classes with funny names—the virtual machine knows nothing at all about them. To solve this mystery, let’s again use the ReflectionTest program to spy on the TalkingClock class:

class TalkingClock
{
   private int interval;
   private boolean beep;

   public TalkingClock(int, boolean);
   static boolean access$0(TalkingClock);
   public void start();
}

Notice the static access$0 method that the compiler added to the outer class. It returns the beep field of the object that is passed as a parameter. (The method name might be slightly different, such as access$000, depending on your compiler.)

The inner class methods call that method. The statement

if (beep)

in the actionPerformed method of the TimePrinter class effectively makes the following call:

if (access$0(outer));

Is this a security risk? You bet it is. It is an easy matter for someone else to invoke theaccess$0 method to read the private beep field. Of course, access$0 is not a legal name for a Java method. However, hackers who are familiar with the structure of class files can easily produce a class file with virtual machine instructions to call that method, for example, by using a hex editor. Since the secret access methods have package visibility, the attack code would need to be placed inside the same package as the class under attack.

To summarize, if an inner class accesses a private data field, then it is possible to access that data field through other classes added to the package of the outer class, but to do so requires skill and determination. A programmer cannot accidentally obtain access but must intentionally build or modify a class file for that purpose.

Local Inner Classes

If you look carefully at the code of the TalkingClock example, you will find that you need the name of the type TimePrinter only once: when you create an object of that type in the start method.

In a situation like this, you can define the class locally in a single method.

public void start()
{
   class TimePrinter implements ActionListener
   {
      public void actionPerformed(ActionEvent event)
      {
          Date now = new Date();
          System.out.println("At the tone, the time is " + now);
          if (beep) Toolkit.getDefaultToolkit().beep();
      }
   }
   ActionListener listener = new TimePrinter();
   Timer t = new Timer(interval, listener);
   t.start();
}

Local classes are never declared with an access specifier (that is, public or private). Their scope is always restricted to the block in which they are declared.

Local classes have one great advantage: They are completely hidden from the outside world—not even other code in the TalkingClock class can access them. No method except start has any knowledge of the TimePrinter class.

Local classes have another advantage over other inner classes. Not only can they access the fields of their outer classes; they can even access local variables! However, those local variables must be declared final. Here is a typical example. Let’s move the interval and beep parameters from the TalkingClock constructor to the start method.

public void start(int interval, final boolean beep)
{
   class TimePrinter implements ActionListener
   {
      public void actionPerformed(ActionEvent event)
      {
          Date now = new Date();
          System.out.println("At the tone, the time is " + now);
          if (beep) Toolkit.getDefaultToolkit().beep();
      }
   }

   ActionListener listener = new TimePrinter();
   Timer t = new Timer(interval, listener);
   t.start();
}

For the code in the actionPerformed method to work, the TimePrinter class must have copied the beep field, as a local variable of the start method, before the beep parameter value went away. That is indeed exactly what happens. In our example, the compiler synthesizes the name TalkingClock$1TimePrinter for the local inner class. If you use the ReflectionTest program again to spy on the TalkingClock$1TimePrinter class, you will get the following output:

class TalkingClock$1TimePrinter
{
   TalkingClock$1TimePrinter(TalkingClock, boolean);
   public void actionPerformed(java.awt.event.ActionEvent);
   final boolean val$beep;
   final TalkingClock this$0;
}

Note the boolean parameter to the constructor and the val$beep instance variable. When an object is created, the value beep is passed into the constructor and stored in the val$beep field. The compiler detects access of local variables, makes matching instance fields for each one of them, and copies the local variables into the constructor so that the instance fields can be initialized.

The final restriction is somewhat inconvenient. Suppose, for example, that you want to update a counter in the enclosing scope. Here, we want to count how often the compareTo method is called during sorting:

int counter = 0;
Date[] dates = new Date[100];
for (int i = 0; i < dates.length; i++) 
   dates[i] = new Date()
      {
         public int compareTo(Date other)
         {
            counter++; // ERROR
            return super.compareTo(other);
         }
      };
Arrays.sort(dates);
System.out.println(counter + " comparisons.");

You can’t declare counter as final because you clearly need to update it. You can’t replace it with an Integer because Integer objects are immutable. The remedy is to use an array of length 1:

final int[] counter = new int[1];
for (int i = 0; i < dates.length; i++)
   dates[i] = new Date()
      {
         public int compareTo(Date other)
         {
            counter[0]++;
            return super.compareTo(other);
         }
      };

批注:这是一个Workaround,不改变数组,但可以改变数组中元素的引用值。

Anonymous Inner Classes

When using local inner classes, you can often go a step further. If you want to make only a single object of this class, you don’t even need to give the class a name. Such a class is called an anonymous inner class.

public void start(int interval, final boolean beep)
{
   ActionListener listener = new ActionListener()
      {
         public void actionPerformed(ActionEvent event)
         {
            Date now = new Date();
            System.out.println("At the tone, the time is " + now);
            if (beep) Toolkit.getDefaultToolkit().beep();
         }
      };
    Timer t = new Timer(interval, listener);
    t.start();
}

This syntax is very cryptic indeed. What it means is this: Create a new object of a class that implements the ActionListener interface, where the required method actionPerformed is the one defined inside the braces { }.

In general, the syntax is

new SuperType(construction parameters)
   {
     inner class methods and data
   }

Here, SuperType can be an interface, such as ActionListener; then, the inner class implements that interface. SuperType can also be a class; then, the inner class extends that class.

Reference