第07部分:嵌套类型

目前见到的类、接口和枚举类型都定义为顶层类型。也就是说,都是包的直接成员,独立于其他类型。不过,类型还可以嵌套在其他类型中定义。这种类型是嵌套类型(nested type),一般称为“内部类”,是 Java 语言的一个强大功能。

嵌套类型有两个独立的目的,但都和封装有关。

如果某个类型需要特别深入地访问另一个类型的内部实现,可以嵌套定义这个类型。作为成员类型的嵌套类型,其访问方式与访问成员变量和方法的方式一样,而且能打破封装的规则。

某个类型可能只在特定的情况下需要使用,而且只在非常小的代码区域使用。这个类型应该密封在一个小范围内,因为它其实是实现细节的一部分,应该封装在一个系统的其他部分无法接触到的地方。

嵌套类型也可以理解为通过某种方式和其他类型绑定在一起的类型,不作为完全独立的实体真实存在。类型能通过四种不同的方式嵌套在其他类型中。

静态成员类型

静态成员类型是定义为其他类型静态成员的类型。嵌套的接口、枚举和注解始终都是静态成员类型(就算不使用 static 关键字也是)。

非静态成员类

“非静态成员类型”就是没使用 static 声明的成员类型。只有类才能作为非静态成员类型。

局部类

局部类是在 Java 代码块中定义的类,只在这个块中可见。接口、枚举和注解不能定义为局部类型。

匿名类

匿名类是一种局部类,但对 Java 语言来说没有有意义的名称。接口、枚举和注解不能定义为匿名类型。

“嵌套类型”这个术语虽然正确且准确,但开发者并没有普遍使用,大多数 Java 程序员使用的是一个意义模糊的术语——“内部类”。根据语境的不同,这个术语可以指代非静态成员类、局部类或匿名类,但不能指代静态成员类型,因此使用“内部类”这个术语时无法区分指代的是哪种嵌套类型。

虽然表示各种嵌套类型的术语并不总是那么明确,但幸运的是,从语境中一般都能确定应该使用哪种句法。

下面详细介绍这四种嵌套类型。每种类型都用单独的一节介绍其特性,使用时的限制,以及专用的 Java 句法。介绍完这四种嵌套类型之后,还有一节说明嵌套类型的运作方式。




静态成员类型

静态成员类型和普通的顶层类型很像,但为了方便,把它嵌套在另一个类型的命名空间中。静态成员类型有如下的基本特性。

静态成员类型类似于类的其他静态成员:静态字段和静态方法;

静态成员类型和所在类的任何实例都不关联(即没有this对象);

静态成员类型只能访问所在类的静态成员;

静态成员类型能访问所在类型中的所有静态成员(包括其他静态成员类型);

不管使不使用 static 关键字,嵌套的接口、枚举和注解都隐式声明为静态类型;

接口或注解中的嵌套类型也都隐式声明为静态类型;

静态成员类型可以在顶层类型中定义,也可以嵌入任何深度的其他静态成员类型中;

静态成员类型不能在其他嵌套类型中定义。


下面通过一个简单的例子介绍静态成员类型的句法。示例定义了一个辅助接口,是所在类的静态成员。这个示例还展示了如何在定义这个接口的类内部以及外部的类中使用这个接口。注意,在外部类中要使用这个接口在层次结构中的名称。

静态成员类型能访问所在类型中的所有静态成员,包括私有成员。反过来也成立:所在类型的方法能访问静态成员类型中的所有成员,包括私有成员。静态成员类型甚至能访问任何其他静态成员类型中的所有成员,包括这些类型的私有成员。静态成员类型使用其他静态成员时,无需使用所在类型的名称限定成员的名称。

静态成员类型不能和任何一个外层类同名。而且,静态成员类型只能在顶层类型和其他静态成员类型中定义,也就是说,静态成员类型不能在任何成员类、局部类或匿名类中定义。

顶层类型可以声明为 public 或对包私有(即声明时没使用 public 关键字)。但是把顶层类型声明为 private 或 protected 都没什么意义——protected 和对包私有其实一样,而任何其他类型都不能访问声明为 private 的顶层类。

然而,静态成员类型是一种成员,因此所在类型中的成员能使用的访问控制修饰符,静态成员类型都能使用。这些修饰符对静态成员类型来说,作用与用在类型的其他成员上一样。前面说过,接口(和注解)的所有成员都隐式声明为 public,所以嵌套在接口或注解类型中的静态成员类型不能声明为 protected 或 private。

例如,在示例中,Linkable 接口声明为 public,因此任何想存储 LinkedStack 对象的类都可以实现这个接口。

在所在类外部,静态成员类型的名称由外层类型的名称和内层类型的名称组成(例如,LinkedStack.Link able)。

大多数情况下,这种句法有助于提醒内层类和所在的类型有内在联系。不过,Java 语言允许使用 import 指令直接或间接导入静态成员类型:

还可以使用 import static 指令导入静态成员类型。

但是,导入嵌套类型模糊了这个类型和外层类型之间的关系,而这种关系往往很重要,因此很少这么做。







非静态成员类

非静态成员类声明为外层类或枚举类型的成员,而且不使用 static 关键字:

如果把静态成员类型比作类字段或类方法,那么非静态成员类可以比作实例字段或实例

方法;

只有类才能作为非静态成员类型;

一个非静态成员类的实例始终关联一个外层类型的实例;

非静态成员类的代码能访问外层类型的所有字段和方法(静态和非静态的都能访问);

为了让非静态成员类访问外层实例,Java 提供了几个专用的句法。

示例展示了如何定义和使用成员类。这个示例以前面定义的 LinkedStack 类为基础,增加了 iterator() 方法。这个方法返回一个实现 java.util.Iterator 接口的实例,枚举栈中的元素。实现这个接口的类定义为一个成员类。

注意,LinkedIterator 类嵌套在 LinkedStack 类中。因为 LinkedIterator 是辅助类,只在LinkedStack 类中使用,所以在离外层类很近的地方定义,能清晰地表达设计意图——介绍嵌套类型时说过这一点。

1. 成员类的特性

与实例字段和实例方法一样,非静态成员类的每个实例都和外层类的一个实例关联。也就是说,成员类的代码能访问外层类实例的所有实例字段和实例方法(以及静态成员),包括声明为 private 的实例成员。

这个重要的特性在上面示例中已经体现出来了。下面再次列出构造方法 LinkedStack.

LinkedIterator():

public LinkedIterator() { current = head; }

这一行代码把内层类的 current 字段设为外层类中 head 字段的值。即便 head 是外层类的私有字段,也不影响这行代码的正常运行。

非静态成员类和类的任何成员一样,可以使用一个标准的访问控制修饰符。在上面示例中,LinkedIterator 类声明为 protected,所以使用 LinkedStack 类的代码(不同包)不能访问LinkedIterator 类,但是 LinkedStack 的子类可以访问。

2. 成员类的限制

成员类有两个重要的限制。

• 非静态成员类不能和任何外层类或包同名。这是一个重要的规则,但不适用于字段和方法。

• 非静态成员类不能包含任何静态字段、方法或类型,不过可以包含同时使用 static 和final 声明的常量字段。

静态成员是顶层结构,不和任何特定的对象关联,而非静态成员类和外层类的实例关联。在成员类中定义顶层静态成员会让人困惑,因此禁止这么做。

3. 成员类的句法

成员类最重要的特性是可以访问外层对象的实例字段和方法。从示例 4-2 中的构造方法LinkedStack.LinkedIterator() 可以看出这一点:

public LinkedIterator() { current = head; }

在这个示例中,head 字段是外层 LinkedStack 类的字段,我们把这个字段的值赋值给LinkedIterator 类的 current 字段(current 是非静态成员类的一个成员)。

如果想使用this 显式引用,就要使用一种特殊的句法,显式引用 this 对象表示的外层实例。例如,如果想在这个构造方法中显式引用,可以使用下述句法:

public LinkedIterator() { this.current = LinkedStack.this.head; }

这种句法的一般形式是 classname.this,其中 classname 是外层类的名称。注意,成员类中可以包含成员类,嵌套的层级不限。然而,因为成员类不能和任何外层类同名,所以,在 this 前面使用外层类的名称是引用任何外层实例最好的通用方式。

仅当引用的外层类成员被成员类的同名成员遮盖时才必须使用这种特殊的句法。






局部类

局部类在一个 Java 代码块中声明,不是类的成员。只有类才能局部定义,接口、枚举类型和注解类型都必须是顶层类型或静态成员类型。局部类往往在方法中定义,但也可以在类的静态初始化程序或实例初始化程序中定义。

因为所有 Java 代码块都在类中,所以局部类都嵌套在外层类中。因此,局部类和成员类有很多共同的特性。局部类往往更适合看成完全不同的嵌套类型。

局部类的典型特征是局部存在于一个代码块中。和局部变量一样,局部类只在定义它的块中有效。下面示例修改 LinkedStack 类的 iterator() 方法,把 LinkedIterator 类从成员类改成局部类。

这样修改之后,我们把 LinkedIterator 类的定义移到离使用它更近的位置,希望更进一步提升代码的清晰度。简单起见,下面示例只列出了 iterator() 方法,没有写出包含它的整个 LinkedStack 类。

1. 局部类的特性

局部类有如下两个有趣的特性:

• 和成员类一样,局部类和外层实例关联,而且能访问外层类的任何成员,包括私有成员;

• 除了能访问外层类定义的字段之外,局部类还能访问局部方法的作用域中声明为 final的任何局部变量、方法参数和异常参数。

2. 局部类的限制

局部类有如下限制。

局部类的名称只存在于定义它的块中,在块的外部不能使用。(但是要注意,在类的作用域中创建的局部类实例,在这个作用域之外仍能使用。稍后本节会详细说明这种情况。)

局部类不能声明为 public、protected、private 或 static。

与成员类的原因一样,局部类不能包含静态字段、方法或类。唯一的例外是同时使用static 和 final 声明的常量。

接口、枚举类型和注解类型不能局部定义。

局部类和成员类一样,不能与任何外层类同名。

前面说过,局部类能使用同一个作用域中的局部变量、方法参数和异常参数,但这些变量或参数必须声明为 final。这是因为,局部类实例的生命周期可能比定义它的方法的执行时间长很多。

局部类用到的每个局部变量都有一个私有内部副本(这些副本由 javac 自动生成)。只有把局部变量声明为 final 才能保证局部变量和私有副本始终保持一致。

3. 局部类的作用域

介绍非静态成员类时,我们知道,成员类能访问继承自超类的任何成员以及外层类定义的任何成员。这对局部类来说也成立,但局部类还能访问声明为 final 的局部变量和参数。下面示例展示了局部变量能访问的不同字段和变量种类:







词法作用域和局部变量

局部变量在一个代码块中定义,这个代码块是这个变量的作用域,在这个作用域之外无法访问这个局部变量,局部变量也不复存在。花括号划定块的边界,花括号中的任何代码都能使用这个块中定义的局部变量。

这种作用域是词法作用域,定义变量能在哪一块源码种使用。程序员一般可以把这种作用域理解为暂时存在的事物,而不能认为局部变量的存在时间是从 JVM 开始执行代码块开始,到退出代码块为止。像这样理解局部变量和它的作用域一般是合理的。

但是,局部类的出现把这个局面搅乱了。注意,局部类的实例可能在 JVM 退出定义这个局部类的代码块后依然存在,这就是原因。

也就是说,如果创建了局部类的一个实例,那么,JVM 执行完定义这个类的代码块后,实例不会自动消失。因此,即便这个类在局部定义,但这个类的实例能跳出定义它的地方。

这可能会导致一些效果,让某些初次接触的开发者惊讶。这是因为,局部类能使用局部变量,而且会从不复存在的词法作用域中创建变量值的副本。这一点从下述代码种可以看出:

为了理解这段代码,要记住一点,局部类中方法的词法作用域与解释器进出定义局部类的代码块没有任何联系。

局部类的各个实例用到的每个 final 局部变量,都会自动创建一个私有副本,因此,得到的效果是,创建实例时,这个实例拥有一个所在作用域的私有副本。

局部类 MyIntHolder 有时也叫闭包(closure)。用更一般的 Java 术语来说,闭包是一个对象,它保存作用域的状态,并让这个作用域在后面可以继续使用。

在某些编程风格中闭包是有用的。不同的编程语言使用不同的方式定义和实现闭包,Java通过局部类、匿名类和 lambda 表达式实现闭包。






匿名类

匿名类是没有名称的局部类,使用 new 运算符在一个简洁的表达式中定义和实例化。局部类是 Java 代码块中的一个语句,而匿名类是一个表达式,因此可以包含在大型表达式中,例如方法调用表达式。

为了完整介绍嵌套类,这里涵盖了匿名类,但在 Java 8 之后,大多数情况下都把匿名类换成了 lambda 表达式。

下面示例在 LinkedStack 类的 iterator() 方法中使用匿名类实现 LinkedIterator 类。和上面示例对比一下,上面示例使用局部类实现了同一个类。

可以看出,定义匿名类和创建这个类的实例使用 new 关键字,后面跟着某个类的名称和放在花括号里的类主体。如果 new 关键字后面是一个类的名称,那么这个匿名类是指定类的子类。如果 new 关键字后面是一个接口的名称,如前面的示例所示,那么这个匿名类实现指定的接口,并且扩展 Object 类。

匿名类使用的句法无法指定 extends 子句和 implements 子句,也不能为这个类指定名称。

因为匿名类没有名称,所以不能在类主体中定义构造方法。这是匿名类的一个基本限制。定义匿名类时,在父类后面的括号中指定的参数,会隐式传给父类的构造方法。匿名类一般用于创建构造方法不接受任何参数的简单类的子类,所以,在定义匿名类的句法中,括号经常都是空的。前面示例中的匿名类实现一个接口并扩展 Object 类。因为构造方法Object() 不接受参数,所以括号是空的。

匿名类的限制

匿名类就是一种局部类,所以二者的限制一样。除了使用 static final 声明的常量之外,匿名类不能定义任何静态字段、方法和类。接口、枚举类型和注解类型不能匿名定义。而且,和局部类一样,匿名类不能声明为 public、private、protected 或 static。

定义匿名类的句法既定义了这个类也实例化了这个类。如果每次执行外层块时创建的实例不止一个,那么就不能用匿名类代替局部类。

因为匿名类没有名称,所以无法为匿名类定义构造方法。如果类需要构造方法,必须使用局部类。不过,经常可以使用实例初始化程序代替构造方法。

虽然实例初始化程序不仅限于在匿名类中使用,但就是为了这个目的才把这种功能引入 Java 语言的。匿名类不能定义构造方法,所以只有一个默认构造方法。使用实例初始化程序可以打破匿名类不能定义构造方法这个限制。







嵌套类型的运作方式

前面说明了这四种嵌套类型的特性和行为。对于嵌套类型,尤其只是为了使用,你要知道的就这么多。不过,理解嵌套类型的运作方式后能更好地理解嵌套类型。

引入嵌套类型后,Java 虚拟机和 Java 类文件的格式并没有变化。对 Java 解释器而言,并没有所谓的嵌套类型,所有类都是普通的顶层类。

为了让嵌套类型看起来是在另一个类中定义的,Java 编译器会在它生成的类中插入隐藏字段、方法和构造方法参数。这些隐藏字段和方法经常称为合成物(synthetic)。

你可以使用反汇编程序 javap(第 13 章会介绍)反汇编某些嵌套类型的类文件,了解为了支持嵌套类型,编译器用了什么技巧。

为了实现嵌套类型,javac 把每个嵌套类型编译为单独的类文件,得到的其实是顶层类。编译得到的类文件使用特殊的命名约定,这些名称一般在用户的代码中无法创建。

在第一个 LinkedStack 类的示例(示例 4-1)中,定义了一个名为 Linkable 的静态成员接口。编译这个 LinkedStack 类时,编译器会生成两个类文件,第一个是预期的 LinkedStack.class。

不过,第二个类文件名为 LinkedStack$Linkable.class,其中,$ 由 javac 自动插入。这个类文件中包含的就是静态成员接口 Linkable 的实现。

因为嵌套类型编译成普通的顶层类,所以不能直接访问外层类型中有特定权限的成员。因此,如果静态成员类型使用了外层类型的私有成员(或具有其他权限的成员),编译器会生成合成的访问方法(具有默认的包访问权限),然后把访问私有成员的表达式转换成调用合成方法的表达式。

这四种嵌套类型的类文件使用如下命名约定。

(静态或非静态)成员类型:根据 EnclosingType$Member.class 格式命名成员类型的类文件。

匿名类:因为匿名类没有名称,所以类文件的名称由实现细节决定。Oracle/OpenJDK 中的 javac使用数字表示匿名类的名称(例如 EnclosingType$1.class)。

局部类:局部类的类文件综合使用前两种方式命名(例如 EnclosingType$1Member.class)。

下面简单介绍一些实现细节,看一下 javac 如何为每种嵌套类型提供所需的合成访问能力。

1. 非静态成员类的实现

非静态成员类的每个实例都和一个外层类的实例关联。为了实现这种关联,编译器为每个成员类定义了一个名为 this$0 的合成字段。这个字段的作用是保存一个外层实例的引用。

编译器为每个非静态成员类的构造方法提供了一个额外的参数,用于初始化这个字段。每次调用成员类的构造方法时,编译器都会自动把这个额外参数的值设为外层类的引用。

2. 局部类和匿名类的实现

局部类之所以能访问外层类的字段和方法,原因和非静态成员类一模一样:编译器把一个外层类的隐藏引用传入局部类的构造方法,并且把这个引用存储在编译器合成的一个私有字段中。和非静态成员类一样,局部类也能使用外层类的私有字段和方法,因为编译器会插入任何所需的访问器方法。

局部类和成员类的不同之处在于,局部类能访问所在块中的局部变量。不过这种能力有个重要的限制,即局部类只能访问声明为 final 的局部变量和参数。这个限制的原因从实现中可以清楚地看出来。

局部类之所以能使用局部变量,是因为 javac 自动为局部类创建了私有实例字段,保存局部类用到的各个局部变量的副本。

编译器还在局部类的构造方法中添加了隐藏的参数,初始化这些自动创建的私有字段。其实,局部类没有访问局部变量,真正访问的是局部变量的私有副本。如果在局部类外部能修改局部变量,就会导致不一致性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,484评论 1 51
  • 特别喜欢《我是演说家》这个节目,看到各位选手在台上洒脱自如地说出自己的观点、讲出自己的故事,用他们的真诚和魅力打动...
    26_lily李_d411阅读 174评论 0 0
  • 文/孟小满 (一) “儿子,你好好考试,妈妈等你拿到面试通知。” “儿子,你放心,不管要多少钱,老爸都会寄给你,你...
    孟小满阅读 1,747评论 94 121
  • 江南可采莲 莲叶何田田 鱼戏莲叶间 鱼戏莲叶东 鱼戏莲叶西 鱼戏莲叶南 鱼戏莲叶北。
    笑妈Darling阅读 140评论 0 1
  • 今年目标:健康管理(早睡11点之前、饮食、体重) 蓝色ATENZA 找到一位合作伙伴 今日青蛙:1.阅读。2.听音...
    镇星Aquarius阅读 191评论 0 0