0%

Java专题:嵌套类

简介

Java语言允许一个类定义在另一个类中,处于内层的类称为嵌套类(nested class), 处于外层的类称为包围类(enclosing class)、外围类或外部类。

嵌套是一种类之间的关系,而不是对象之间的关系。

分类

嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类。除第一种外,其它称为内部类。

局部类是声明在语句块中的内部类,常见于方法体中;匿名类是无名内部类。注意它们都不是包围类成员。

使用原因

之所以使用嵌套类,主要出于以下几点原因:

  • 逻辑分组。如果一个类只对唯一的另一个类有用,那么这个类应该嵌入另一个类中以保持两个类在一起。 嵌套的帮助类使得包更精简。
  • 增强封装。私有的嵌套类除其包围类外不可见,避免了以公共API的形式暴露,具有更严格的封装。
  • 更具可读性和可维护性。嵌套类临近其被使用的地方,使代码更易理解和维护。

需要嵌套类的原因:每个嵌套类都能独立地继承自一个(接口的)实现, 所以无论包围类是否已经继承了某个(接口的)实现,对于嵌套类都没有影响。

局部类名字在外部(如方法外)不可见,使用它的唯一理由是:需要一个已命名的构造器, 或者需要重载构造器,而匿名类只能用于实例初始化。

匿名类能使代码更简洁。同时声明及实例化一个类。当一个局部类只使用一次时,应使用匿名类。

语法

创建对象

静态成员类 new OuterClass.StaticNestedClass()

内部类 outerInstance.new InnerClass()

成员访问

通常可以通过成员名称直接访问,如果存在遮蔽的情况,应加限定:

嵌套类 OuterClass.member

内部类 OuterClass.this.member

Class 文件名

(非)静态成员类 局部类 匿名类
Class 文件名 OuterClass$InnerClass OuterClass$nInnerClass OuterClass$n

①局部类class文件名格式为 OuterClass$nInnerClass.class,与(非)静态成员类相比多了一个数字, 这是因为同一个类里不可能有两个同名的成员类,而同一个类里则可能有多个同名的局部类, 因而用数字区分。

②匿名类没有类名,所以只能以数字区分。

特殊性

可访问性

除了静态成员类不能访问包围类的实例成员外,其它内部类都可以访问所有包围类成员,即使是私有的。 反过来,包围类通过嵌套类实例也可以访问其私有成员。

遮蔽(shadow)

在一个特定范围(比如内部类或方法声明)中,两个声明同名,此时一个声明将遮蔽另一个, 即简单名将引用到作用域较小的那个声明上。 一个变量、方法或类型可以分别遮蔽在一个特定范围内具有相同名字的所有变量、方法或类型。

对于嵌套类而言,其变量、方法或类型都可能分别遮蔽外围类的变量、方法或类型。

序列化(Serialization)

强烈反对所有内部类的序列化。因为编译器会创建合成构造—— 使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用及外围作用域的局部变量的值 ——但是它在不同Java编译器实现中是不同的,这意味着, .class文件也会不同。在不同JRE实现间进行序列化(serialize)和反序列化(deserialize)可能会引起兼容性问题。

编译器修改了所有的内部类的构造器,添加了一个外围类引用的参数,并为外围类引用生成了一个实例域,由构造参数传入设置。

编译器必须检测对局部变量的访问,为每一个变量建立相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。

注:合成构造(synthetic constructs)使Java编译器能够实现新的Java特性而又不改变JVM。

类名冲突

所有嵌套类都可能存在命名冲突的问题。根据嵌套类的Class文件名,如果某类与其外围类同包,且类名与嵌套类Class文件同名, 那么将出现命名冲突,冲突的两个类不能同时编译。一般来说,类名应该尽量避免使用“$”,这将有效规避该问题。

关于接口

嵌套接口

通常所说的嵌套类的范围比字面上要广,还应包括嵌套接口。

嵌套接口是隐式静态的,因此只能声明在顶级类、接口或静态上下文中。

“隐式”是一种缺省形式,代表拥有不可变更的默认值。比如上文中的“隐式静态”,即指嵌套接口不论是否使用 static 修饰,都必然是静态的。

接口中的嵌套类

接口的成员是隐匿静态的,所以其成员类一定是静态嵌套类。

一种不常见的情况是,在接口的缺省(default)方法中定义内部类,因而接口中声明内部类是可能的,只是它们将不是成员类。

限制

嵌套接口是隐式静态的,因此不能在方法、语句块等非静态上下文中声明接口。

内部类中不能声明静态成员。

静态成员包括静态域、静态方法、静态初始化器以及静态类型。其中要注意,由于嵌套接口是隐式静态的,因此内部类中必然不能声明接口。

匿名类与正规的继承相比有些受限,因为匿名类可以扩展类,也可以实现接口,但是不能两者兼备。如果是实现接口,也只能实现一个接口。

匿名类不能定义构造器,因为没有类名。取而代之的是,将构造器参数传递给超类(superclass)构造器。尤其是在匿名类实现接口的时候,不能有任何构造参数。

局部/匿名类访问外围类的局部变量,如果仅在局部/匿名类构造参数中访问,则局部变量不必是 final 的,否则必须使用 final 修饰。

Java8 放宽了这一限制,要求局部变量必须是 final 的或事实上是 final 的。所谓“事实上是”是指虽然没有 final 修饰,但代码中并没有为该局部变量重新赋值。

例外

内部类可以有静态成员,只要它是常量。

常量是一个原始类型或字符串类型的变量,其声明为 final,并使用编译期(compile-time)常量表达式初始化。

编译期常量表达式通常是一个字符串或一个能在编译期求值的算数表达式,即字面量或字面量运算表达式。

内部类可以继承不是常量的静态成员,尽管内部类不能声明它们。

虽然,接口中的成员类一定是静态嵌套类,但 Java 8 允许接口声明缺省方法,这使得在接口中声明内部类成为可能。

其它说明

接口的成员是隐式静态的,因此声明在其中的类不是内部类,而是静态嵌套类。

嵌套类可以定义在接口中,并隐式地被 public static 修饰,甚至还可以实现包围接口。

一个内部类被嵌套多少层并不重要,它能透明地访问所有它所嵌入的外围类的所有成员。

内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用 $ 分隔外部类名与嵌套类名的常规类文件,而虚拟机对此一无所知。

内部类系统在编译阶段会为构造器增加一个外围类参数作为第一参数。

匿名类末尾的分号,不是用来标记此内部类结束的。标记的是表达式的结束,只不过这个表达式正巧包含了匿名类罢了。

匿名类仅仅添加实例成员是没有意义的,因为它的上层“接口”不会暴露新添加的成员。不过,新添加的成员可以在重写的方法中被使用, 这可以使程序更具灵活性。

闭包:一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。内部类是面向对象的闭包。

示例

遮蔽(shadow)

1
2
3
4
5
6
7
8
9
public class ShadowTest 
{
public int x = 0;
class FirstLevel
{
public int x = 1;

}
}

说明:在 FirstLevel中, x 将引用到其自己的成员 x 上,而遮蔽其外围类成员 x。要引用外围类成员 x,需要限定为 ShadowTest.this.x

外围类继承内部类

内部类的子类不一定是内部类,它可以了是一个顶级类(top-level class)。但内部类的子类实例一样需要保留一个引用,该引用指向其父类所在外围类的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OuterClass
{
class InnerClass
{
}
}

class SubInnerClass extends OuterClass. InnerClass
{
public SubIn(OuterClass outerInstance )
{
outerInstance.super(); // ①
}
}

① 内部类需要一个外围类对象来调用构造方法,这个外围类对象通常是隐式传入的,并在编译阶段为构造器增加一个外围类参数作为第一参数。 但是内部类的子类如果与父类不在同一个外围类中或者它就是一个顶级类,那么它将不能隐式访问外围类对象,所以需要显式的在构造方法中传一个外围类对象参数, 该参数在参数列表里的位置是任意的。并且在构造方法的第一行需要使用外围类对象参数来调用父类构造方法。

匿名类访问外部对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class Base
{
public Base( int intvar )
{
}
public abstract void access();
}

class Outer
{
public static Base getBase( int intvar, final String str, final int intvar2, int intvar3 )
{
return new Base( intvar① )
{
public void access()
{
System.out.println( str② );
System.out.println( intvar2 );
}
};
}
}

① 外部变量只在构造方法参数列表中出现,那么不必是 final 修饰的,因为该变量仅被传递给匿名类的基类构造方法,不会在匿名类内部直接使用。

② 匿名类访问了外部变量,那么编译器要求该变量是 final 修饰的。Java 8 放宽了这一要求,只要变量事实上是 final 就可以不用 final 修饰。

附录

谬称

对于嵌套类而言,日常使用、交流乃至文档教材中其概念、名称及分类上都有诸多谬误。个人猜测,这种情况可能是由于翻译问题,或者概念混淆后的以讹传讹。

内部类概念范围扩大。首先,很多地方会使用内部类来指代嵌套类,需要明确的是,嵌套类的范围要比内部类大,嵌套类才是这一系列类型的统称。 属于这类的谬称有:静态内部类、静态成员内部类、类成员内部类。

内部类概念范围缩小。其次,相对于扩大内部类概念范围的情况,缩小其概念范围的情况也是常见的,通常使用内部类来指代非静态成员类,而排除了匿名类和局部类。 事实上,内部类是包括非静态成员类、匿名类和局部类的。

通俗命名

在实际使用中,嵌套类及其分类还有很多通俗的名称或别名,这与谬称不同,它们并没有扩大或缩小概念本身的范围。

局部内部类、匿名内部类:分别是局部类和匿名类的通俗叫法。这种把简单名扩展成长名称的方式很特别,或许是为了突出其内部类的属性。

非静态内部类、非静态嵌套类:概念同“内部类”,与谬称“静态内部类”相对,但它不属于谬称,因为内部类理应不是静态的。 但是,应注意的是,在某些语境中可能会排除局部类和匿名类,这属于“内部类概念范围缩小”的变体。

静态嵌套类:指静态成员类。

成员类:概括指静态成员类和非静态成员类。

非静态成员内部类、实例成员内部类:指非静态成员类。

编程技巧

双括号初始化(double brace initialization)

应用场景:快捷创建一个列表对象。

1
invite(new ArrayList<String>() {{ add(“Eric”); add(“Zong”) }}); 

说明:外层括号建立了ArrayList的一个匿名子类,内层括号则是一个对象构造块。