corejava11(6.3 内部类)

6.3 内部类

内部类是在另一个类中定义的类。你为什么要这么做?有两个原因:

  • 内部类可以对同一包中的其他类隐藏。
  • 内部类方法可以从定义它们的作用域访问数据,包括那些原本是私有的数据。

内部类过去对于简洁地实现回调非常重要,但现在lambda表达式做得更好。不过,内部类对于构建代码非常有用。以下部分将引导您了解所有详细信息。

C++注意

C++有嵌套类。嵌套类包含在封闭类的范围内。这里有一个典型的例子:链表类定义了一个用来保存链接的类,以及一个用来定义迭代器位置的类。

class LinkedList
{ 
public:
    class Iterator // a nested class
    { 
    public:
        void insert(int x);
        int erase();
        . . .
    private:
        Link* current;
        LinkedList* owner;
    };
    . . .
private:
    Link* head;
    Link* tail;
};

嵌套类与Java中的内部类类似。但是,Java内部类有一个额外的特性,使它们比C++中的嵌套类更丰富和更有用。来自内部类的对象对实例化它的外部类对象具有隐式引用。通过这个指针,它可以访问外部对象的总状态。例如,在Java中,Iterator类不需要指向它所指向的LinkedList的显式指针。

在Java中,static内部类不具有此添加的指针。它们是C++中的Java类比嵌套类。

6.3.1 使用内部类访问对象状态

内部类的语法相当复杂。出于这个原因,我们提供了一个简单但有些人为的例子来演示内部类的使用。我们重构Timertest示例并提取TalkingClock类。一个有声时钟由两个参数构成:通知之间的间隔和一个打开或关闭蜂鸣声的标志。

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
    {
    	. . .
    }
}

请注意,TimePrinter类现在位于TalkingClock类中。这并不意味着每个TalkingClock都有一个TimePrinter实例字段。正如您将看到的,TimePrinter对象是由TalkingClock类的方法构造的。

下面是更详细的TimePrinter类。请注意,actionPerformed方法在发出蜂鸣声之前检查beep标志。

public class TimePrinter implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        System.out.println("At the tone, the time is "
        	+ Instant.ofEpochMilli(event.getWhen()));
        if (beep) Toolkit.getDefaultToolkit().beep();
    }
}

发生了令人惊讶的事情。TimePrinter类没有名为beep的实例字段或变量。相反,beep指的是创建这个TimePrinterTalkingClock对象的字段。如您所见,内部类方法可以访问自己的数据字段和创建它的外部对象的数据字段。

为了实现这一点,内部类的对象总是得到对创建它的对象的隐式引用(参见图6.3)。

图6.3 内部类对象具有对外部类对象的引用。

此引用在内部类的定义中不可见。然而,为了阐明这个概念,让我们称之为外部对象的引用。然后,actionPerformed方法等效于以下内容:

public void actionPerformed(ActionEvent event)
{
    System.out.println("At the tone, the time is "
    	+ Instant.ofEpochMilli(event.getWhen()));
    if (outer.beep) Toolkit.getDefaultToolkit().beep();
}

外部类引用在构造函数中设置。编译器修改所有内部类构造函数,为外部类引用添加一个参数。TimePrinter类不定义构造函数;因此,编译器合成一个无参数构造函数,生成如下代码:

public TimePrinter(TalkingClock clock) // automatically generated code
{
	outer = clock;
}

请注意,outer不是Java关键字。我们只是用它来说明一个内部类所涉及的机制。

当在start方法中构造TimePrinter对象时,编译器将this引用传递给当前的会话时钟到构造函数:

var listener = new TimePrinter(this); // parameter automatically added

清单6.7显示了测试内部类的完整程序。再看看访问控制。如果TimePrinter类是常规类,它将需要通过TalkingClock类的公共方法访问beep标志。使用内部类是一种改进。不需要提供只对另一个类感兴趣的访问器。

注意

我们可以将TimePrinter类声明为private。然后,只有TalkingClock方法才能构造TimePrinter对象。只有内部类可以是私有的。常规类总是具有包或公共访问权限。

清单6.7 innerClass/InnerClassTest.java

package innerClass;

import java.awt.*;
import java.awt.event.*;
import java.time.*;

import javax.swing.*;

/**
 * This program demonstrates the use of inner classes.
 * @version 1.11 2017-12-14
 * @author Cay Horstmann
 */
public class InnerClassTest
{
   public static void main(String[] args)
   {
      var clock = new TalkingClock(1000, true);
      clock.start();

      // keep program running until the user selects "OK"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);
   }
}

/**
 * A clock that prints the time in regular intervals.
 */
class TalkingClock
{
   private int interval;
   private boolean beep;

   /**
    * Constructs a talking clock
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   public TalkingClock(int interval, boolean beep)
   {
      this.interval = interval;
      this.beep = beep;
   }

   /**
    * Starts the clock.
    */
   public void start()
   {
      var listener = new TimePrinter();
      var timer = new Timer(interval, listener);
      timer.start();
   }

   public class TimePrinter implements ActionListener
   {
      public void actionPerformed(ActionEvent event)
      {
         System.out.println("At the tone, the time is " 
            + Instant.ofEpochMilli(event.getWhen()));
         if (beep) Toolkit.getDefaultToolkit().beep();
      }
   }
}

6.3.2 内部类的特殊语法规则

在前面的部分中,我们通过调用一个内部类的outer来解释它的外部类引用。实际上,外部引用的正确语法要复杂一些。表达式

OuterClass.this

表示外部类引用。例如,可以将TimePrinter内部类的actionPerformed方法编写为

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

相反,您可以使用语法更显式地编写内部对象构造函数。

outerObject.new InnerClass(construction parameters)
ActionListener listener = this.new TimePrinter();

这里,新构造的TimePrinter对象的外部类引用被设置为创建内部类对象的方法的this引用。这是最常见的情况。像往常一样,this。限定符是多余的。但是,也可以通过显式命名将外部类引用设置为另一个对象。例如,由于TimePrinter是一个公共的内部类,因此可以为任何有声时钟构造一个TimePrinter

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

请注意,您将一个内部类称为

OuterClass.InnerClass

当它发生在外部类的范围之外时。

注意

内部类中声明的任何静态字段都必须是final字段,并用编译时常量初始化。如果字段不是常量,它可能不是唯一的。

内部类不能有static方法。Java语言规范没有给出这种限制的理由。可以允许静态方法只访问封闭类中的静态字段和方法。显然,语言设计者认为复杂性大于好处。

6.3.3 内部类有用吗?真的有必要吗?安全?

当Java语言中的内部类被添加到Java 1.1中时,许多程序员认为它们是一个主要的新特性,体现了Java语言比C++更简单。不可否认,内部类语法非常复杂。(随着我们在本章后面研究匿名内部类,它变得更加复杂。)内部类如何与语言的其他特性(如访问控制和安全性)交互并不明显。

通过添加一个优雅而有趣而非需要的特性,Java开始了一条毁灭了这么多其他语言的毁灭之路吗?

虽然我们不会试图完全回答这个问题,但值得注意的是,内部类是编译器的一种现象,而不是虚拟机。内部类被转换为常规类文件,用$(美元符号)分隔外部和内部类名称,而虚拟机对它们没有任何特殊的了解。

例如,TalkingClock类中的TimePrinter类被转换为TalkingClock$TimePrinter.class类文件。要在工作中看到这一点,请尝试以下实验:运行第5章中的ReflectionTest程序,并给它一个类TalkingClock$TimePrinter来进行反射。或者,只需使用javap实用程序:

javap -private ClassName

注意

如果使用UNIX,请记住在命令行上提供类名时转义$字符。也就是说,运行ReflectionTestjavap程序时使用

java reflection.ReflectionTest innerClass.TalkingClock\$TimePrinter

或者

javap -private innerClass.TalkingClock\$TimePrinter

您将获得以下打印输出:

public class innerClass.TalkingClock$TimePrinter
	implements java.awt.event.ActionListener
{
    final innerClass.TalkingClock this$0;
    public innerClass.TalkingClock$TimePrinter(innerClass.TalkingCloc
    public void actionPerformed(java.awt.event.ActionEvent);
}

您可以清楚地看到编译器生成了一个额外的实例字段,this$0,用于引用外部类。(this$0的名称是由编译器合成的,您不能在代码中引用它。)您还可以看到构造函数的TalkingClock参数。

如果编译器可以自动进行这种转换,您就不能简单地手动编程实现相同的机制吗?让我们试试看。我们会让TimePrinter成为一个常规类,而不是在TalkingClock类里面。在构造TimePrinter对象时,我们将创建它的对象的引用传递给它。

class TalkingClock
{
    . . .
    public void start()
    {
        var listener = new TimePrinter(this);
        var timer = new Timer(interval, listener);
        timer.start();
    }
}
class TimePrinter implements ActionListener
{
    private TalkingClock outer;
    . . .
    public TimePrinter(TalkingClock clock)
    {
    	outer = clock;
    }
}

现在让我们看看actionPerformed方法。它需要访问outer.beep

if (outer.beep) . . . // ERROR

在这里我们遇到了一个问题。内部类可以访问外部类的私有数据,但外部TimePrinter类不能。

因此,内部类确实比常规类更强大,因为它们具有更多的访问权限。

如果内部类被转换为具有有趣名称的常规类,那么您可能会想知道内部类是如何获得这些添加的访问权限的——虚拟机对它们一无所知。为了解决这个谜团,让我们再次使用ReflectionTest程序监视TalkingClock类:

class TalkingClock
{
    private int interval;
    private boolean beep;
    public TalkingClock(int, boolean);
    static boolean access$0(TalkingClock);
    public void start();
}

注意编译器添加到外部类的静态access$0方法。它返回作为参数传递的对象的beep字段。(方法名可能稍有不同,如access$000,具体取决于编译器。)

内部类方法调用该方法。声明

if (beep)

TimePrinter类的actionPerformed方法中,有效地进行以下调用:

if (TalkingClock.access$0(outer))

这是安全风险吗?当然。对于其他人来说,调用access$0方法来读取私有的beep字段是很容易的事情。当然,access$0不是Java方法的合法名称。但是,熟悉类文件结构的黑客可以使用虚拟机指令轻松生成类文件来调用该方法,例如,使用十六进制编辑器。由于私密方法具有包访问权限,因此需要将攻击代码放在与受攻击类相同的包中。

总而言之,如果一个内部类访问一个私有的数据字段,那么可以通过添加到外部类包中的其他类访问该数据字段,但要做到这一点需要技巧和决心。程序员不能意外地获得访问权限,但必须为此目的有意构建或修改类文件。

注意

合成的构造器和方法会非常复杂。(如果您是squeamish,请跳过此注释。)假设我们将TimePrinter转换为一个私有的内部类。虚拟机中没有私有类,因此编译器会生成下一个最好的东西,一个具有包访问权限的类和一个私有构造函数:

private TalkingClock$TimePrinter(TalkingClock);

当然,没有人可以调用该构造函数,所以有第二个具有包访问权限的构造函数:

TalkingClock$TimePrinter(TalkingClock, TalkingClock$1);

这就是第一个。TalkingClock$1类的合成只是为了区分这个构造函数和其他构造函数。

编译器将TalkingClock类的start方法中的构造函数调用转换为

new TalkingClock$TimePrinter(this, null)

6.3.4 局部内部类

如果仔细查看TalkingClock示例的代码,您会发现只需要类型TimePrinter的名称一次:当您在start方法中创建该类型的对象时。

在这种情况下,可以在单个方法中本地定义类。

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

本地类从不使用访问说明符(即public或private)声明。它们的作用域总是局限于声明它们的块。

本地类有一个很大的优势:它们完全隐藏在外部世界中,即使是TalkingClock类中的其他代码也无法访问它们。除了start之外,没有任何方法了解TimePrinter类。

6.3.5 从外部方法访问变量

本地类比其他内部类有另一个优势。它们不仅可以访问外部类的字段,甚至可以访问局部变量!但是,这些局部变量必须是有效的final变量。也就是说,一旦被分配,它们可能永远不会改变。

这是一个典型的例子。让我们将intervalbeep参数从TalkingClock构造函数移到start方法。

public void start(int interval, boolean beep)
{
    class TimePrinter implements ActionListener
    {
        public void actionPerformed(ActionEvent event)
        {
            System.out.println("At the tone, the time is "
            	+ Instant.ofEpochMilli(event.getWhen()));
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    }
    var listener = new TimePrinter();
    var timer = new Timer(interval, listener);
    timer.start();
}

注意,TalkingClock类不再需要存储一个beep实例字段。它只引用start方法的beep参数变量。

也许这并不奇怪。这一行

if (beep) ...

毕竟,最终是在start方法中,所以为什么它不能访问beep变量的值呢?

为了了解为什么这里有一个微妙的问题,让我们更仔细地考虑控制流。

  1. start方法被调用
  2. 对象变量listener是通过调用内部类TimePrinter的构造函数初始化的。
  3. listener引用传递给Timer构造函数,计时器启动,start方法退出。此时,start方法的beep参数变量不再存在。
  4. 一秒钟后,actionPerformed方法执行`if (beep) …

要使actionPerformed方法中的代码工作,TimePrinter类必须在beep参数值消失之前将beep字段复制为start方法的局部变量。这确实就是发生的事情。在我们的示例中,编译器为本地内部类合成名称TalkingClock$1TimePrinter。如果再次使用ReflectionTest程序监视TalkingClock$1TimePrinter类,将得到以下输出:

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

请注意构造函数的boolean参数和val$beep实例变量。创建对象时,值beep被传递到构造函数中,并存储在val$beep字段中。编译器检测对局部变量的访问,为每个变量生成匹配的实例字段,并将局部变量复制到构造函数中,以便初始化实例字段。

6.3.6 匿名内部类

当使用本地内部类时,您通常可以更进一步。如果只想生成此类的单个对象,则甚至不需要为类命名。这样的类称为匿名内部类。

public void start(int interval, boolean beep)
{
    var listener = new ActionListener()
        {
            public void actionPerformed(ActionEvent event)
            {
                System.out.println("At the tone, the time is "
                    + Instant.ofEpochMilli(event.getWhen()));
                if (beep) Toolkit.getDefaultToolkit().beep();
            }
        };
    var timer = new Timer(interval, listener);
    timer.start();
}

这种句法确实非常神秘。这意味着:创建一个实现ActionListener接口的类的新对象,其中所需的actionPerformed方法是在大括号内定义的方法。

一般来说,语法是

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

在这里,父类型可以是一个接口,例如ActionListener;然后,内部类实现该接口。父类型也可以是类;然后,内部类扩展该类。

匿名内部类不能有构造函数,因为构造函数的名称必须与类的名称相同,并且类没有名称。相反,构造参数被提供给超类构造函数。特别是,每当内部类实现接口时,它不能有任何构造参数。不过,您必须提供一组括号,如

new InterfaceType()
    {
    	methods and data
    }

您必须仔细查看类的新对象的构造与扩展该类的匿名内部类的对象的构造之间的区别。

var queen = new Person("Mary");
	// a Person object
var count = new Person("Dracula") { . . . };
	// an object of an inner class extending Person

如果构造参数列表的右括号后跟左大括号,则将定义匿名内部类。

注意

即使匿名类不能有构造函数,也可以提供对象初始化块:

var count = new Person("Dracula")
    {
        { initialization }
        . . .
    };

清单6.8包含带有匿名内部类的Talking Clock程序的完整源代码。如果您将这个程序与清单6.7进行比较,您将看到在这种情况下,匿名内部类的解决方案要短得多,并且希望通过一些实践,可以很容易理解。

多年来,Java程序员通常使用匿名内部类来实现事件侦听器和其他回调。现在,最好使用lambda表达式。例如,可以使用如下lambda表达式更简洁地编写本节开头的start方法:

public void start(int interval, boolean beep)
{
    var timer = new Timer(interval, event -> {
        System.out.println("At the tone, the time is "
        	+ Instant.ofEpochMilli(event.getWhen()));
        if (beep) Toolkit.getDefaultToolkit().beep();
    });
    timer.start();
}

注意

下面的技巧称为双括号初始化,它利用了内部类语法。假设要构造一个数组列表并将其传递给一个方法:

var friends = new ArrayList<String>();
friends.add("Harry");
friends.add("Tony");
invite(friends);

如果您不再需要数组列表,最好是匿名的。但是你如何添加元素呢?方法如下:

invite(new ArrayList<String>() {
	{ 
		add("Harry"); 
		add("Tony"); 
	}
});

注意双大括号。外部大括号构成arraylist的匿名子类。内部大括号是一个对象初始化块(参见第4章)。

在实践中,这种技巧很少有用。更可能的是,invite方法愿意接受任何List<String>,您可以简单地传递List.of("harry", "tony")

小心

制作一个匿名的子类通常很方便,它和它的超类差不多,但不完全一样。但是你需要小心使用equals方法。在第5章中,我们建议您的equals方法使用测试

if (getClass() != other.getClass()) return false;

匿名子类将无法通过此测试。

提示

当您生成日志或调试消息时,您通常希望包括当前类的名称,例如

System.err.println("Something awful happened in " + getClass());

但在静态方法中失败了。毕竟,对getClass的调用,会调用this.getClass(),而静态方法没有this。请改用以下表达式:

new Object(){}.getClass().getEnclosingClass() // gets class of static method

这里,new Object() {}生成Object的匿名子类的匿名对象,getEnclosingClass获取其封闭类,即包含静态方法的类。

清单6.8 anonymousInnerClass/AnonymousInnerClassTest.java

package anonymousInnerClass;

import java.awt.*;
import java.awt.event.*;
import java.time.*;

import javax.swing.*;

/**
 * This program demonstrates anonymous inner classes.
 * @version 1.12 2017-12-14
 * @author Cay Horstmann
 */
public class AnonymousInnerClassTest
{
   public static void main(String[] args)
   {
      var clock = new TalkingClock();
      clock.start(1000, true);

      // keep program running until the user selects "OK"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);
   }
}

/**
 * A clock that prints the time in regular intervals.
 */
class TalkingClock
{
   /**
    * Starts the clock.
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   public void start(int interval, boolean beep)
   {
      var listener = new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               System.out.println("At the tone, the time is " 
                  + Instant.ofEpochMilli(event.getWhen()));
               if (beep) Toolkit.getDefaultToolkit().beep();
            }
         };
      var timer = new Timer(interval, listener);
      timer.start();
   }
}

6.3.7 静态内部类

有时,您可能只想使用一个内部类将一个类隐藏在另一个内部类中,但不需要内部类具有对外部类对象的引用。可以通过声明内部类static来抑制该引用的生成。

下面是一个典型的例子,说明您希望在哪里执行此操作。考虑计算数组中最小值和最大值的任务。当然,您可以编写一个方法来计算最小值,另一个方法来计算最大值。当调用这两个方法时,数组将被遍历两次。只遍历一次数组会更有效,同时计算最小值和最大值。

double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values)
{
    if (min > v) min = v;
    if (max < v) max = v;
}

但是,该方法必须返回两个数字。我们可以通过定义一个包含两个值的类Pair来实现这一点:

class Pair
{
    private double first;
    private double second;
    public Pair(double f, double s)
    {
        first = f;
        second = s;
    }
    public double getFirst() { return first; }
    public double getSecond() { return second; }
}

然后minmax方法可以返回类型Pair的对象。

class ArrayAlg
{
    public static Pair minmax(double[] values)
    {
        . . .
        return new Pair(min, max);
    }
}

方法的调用方使用getFirstgetSecond方法来接收答案:

Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());

当然,名称Pair是一个非常常见的名称,在一个大型项目中,很有可能其他程序员也有同样的好主意,但却创建了一个包含一对字符串的Pair类。我们可以通过在ArrayAlg中使Pair成为公共的内部类来解决这个潜在的名称冲突。那么这个类将被公开为ArrayAlg.Pair

ArrayAlg.Pair p = ArrayAlg.minmax(d);

但是,与前面示例中使用的内部类不同,我们不希望对Pair对象中的任何其他对象进行引用。可以通过声明内部类static来抑制该引用:

class ArrayAlg
{
    public static class Pair
    {
    	. . .
    }
    . . .
}

当然,只有内部类可以声明为静态的。静态内部类与任何其他内部类完全相同,只是静态内部类的对象没有对生成它的外部类对象的引用。在我们的示例中,必须使用静态内部类,因为内部类对象是在静态方法内部构造的:

public static Pair minmax(double[] d)
{
    . . .
    return new Pair(min, max);
}

如果Pair类没有声明为static,编译器就会抱怨没有ArrayAlg类型的隐式对象可用于初始化内部类对象。

注意

只要内部类不需要访问外部类对象,就使用静态内部类。一些程序员使用嵌套类这个术语来描述静态内部类。

注意

与常规内部类不同,静态内部类可以具有静态字段和方法。

注意

在接口内部声明的内部类自动是static的和public的。

清单6.9包含ArrayAlg类和嵌套Pair类的完整源代码。

清单6.9 staticInnerClass/StaticInnerClassTest.java

package staticInnerClass;

/**
 * This program demonstrates the use of static inner classes.
 * @version 1.02 2015-05-12
 * @author Cay Horstmann
 */
public class StaticInnerClassTest
{
   public static void main(String[] args)
   {
      var values = new double[20];
      for (int i = 0; i < values.length; i++)
         values[i] = 100 * Math.random();
      ArrayAlg.Pair p = ArrayAlg.minmax(values);
      System.out.println("min = " + p.getFirst());
      System.out.println("max = " + p.getSecond());
   }
}

class ArrayAlg
{
   /**
    * A pair of floating-point numbers
    */
   public static class Pair
   {
      private double first;
      private double second;

      /**
       * Constructs a pair from two floating-point numbers
       * @param f the first number
       * @param s the second number
       */
      public Pair(double f, double s)
      {
         first = f;
         second = s;
      }

      /**
       * Returns the first number of the pair
       * @return the first number
       */
      public double getFirst()
      {
         return first;
      }

      /**
       * Returns the second number of the pair
       * @return the second number
       */
      public double getSecond()
      {
         return second;
      }
   }

   /**
    * Computes both the minimum and the maximum of an array
    * @param values an array of floating-point numbers
    * @return a pair whose first element is the minimum and whose second element
    * is the maximum
    */
   public static Pair minmax(double[] values)
   {
      double min = Double.POSITIVE_INFINITY;
      double max = Double.NEGATIVE_INFINITY;
      for (double v : values)
      {
         if (min > v) min = v;
         if (max < v) max = v;
      }
      return new Pair(min, max);
   }
}

猜你喜欢

转载自blog.csdn.net/nbda1121440/article/details/91314722
今日推荐