嵌套类
Java 编程语言允许你在另一个类内部定义类。此类称为嵌套类,示例如下:
class OuterClass {
...
class NestedClass {
...
}
}
术语:嵌套类分为两类:非静态类和静态类。非静态嵌套类称为内部类。声明为静态的嵌套类称为静态嵌套类。
class OuterClass {
...
class InnerClass {
...
}
static class StaticNestedClass {
...
}
}
嵌套类是其外围类的成员。非静态嵌套类(内部类)可以访问外部类的其他成员,即使这些成员声明为私有。静态嵌套类则无法访问外部类的其他成员。作为外部类的成员,嵌套类可以声明为 private, public, protected 或 package private 。需注意外部类只能声明为 public 或 package private 。
为什么要使用嵌套类?
使用嵌套类的充分理由包括以下几点:
- 这是一种将仅在单一位置使用的类进行逻辑分组的方法:若某个类仅对另一个类有用,则将其嵌入该类并保持二者关联是合理的。通过嵌套此类”辅助类”,可使相关包结构更为精简。
- 它增强了封装性:考虑两个顶级类A和B,其中B需要访问A的成员,而这些成员原本会被声明为私有。通过将类B隐藏在类A内部,A的成员可以声明为私有,而B仍能访问它们。此外,B本身也可以对外部世界隐藏。
- 这能使代码更易于阅读和维护:将小型类嵌套在顶级类中,使代码更接近其使用位置。
内部类
与实例方法和变量类似,内部类与其外围类的实例相关联,可直接访问该对象的方法和字段。此外,由于内部类与实例相关联,它本身无法定义任何静态成员。
内部类的实例对象存在于外部类的实例中。请考虑以下类:
class OuterClass {
...
class InnerClass {
...
}
}
内部类的实例只能存在于外部类的实例内部,并能直接访问其包含实例的方法和字段。
要实例化一个内部类,必须先实例化其外部类。然后,使用以下语法在外部对象内部创建内部对象:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
内部类有两种特殊类型:局部类和匿名类。
静态嵌套类
与类方法和变量类似,静态嵌套类与其外部类相关联。同样地,静态嵌套类无法直接引用其外部类中定义的实例变量或方法:它只能通过对象引用来使用这些成员。内部类与嵌套静态类的示例对此进行了说明。
注意:静态嵌套类与外部类(及其他类)的实例成员交互的方式,与任何其他顶级类完全相同。实际上,静态嵌套类在行为上就是一个顶级类,为了封装便利而嵌套在另一个顶级类中。《内部类与嵌套静态类示例》也展示了这一点。
静态嵌套类的实例化方式与顶级类相同:
StaticNestedClass staticNestedObject = new StaticNestedClass();
内部类和嵌套静态类示例
以下示例通过 OuterClass 与 TopLevelClass 演示了内部类(InnerClass)、嵌套静态类(StaticNestedClass)和顶级类(TopLevelClass)能够访问 OuterClass 的哪些类成员:
OuterClass.java
public class OuterClass {
String outerField = "Outer field";
static String staticOuterField = "Static outer field";
//内部类和外部类的实例相同,可以访问外部类的所有成员
class InnerClass {
void accessMembers() {
IO.print(outerField);
IO.println(staticOuterField);
}
}
// 静态嵌套类,不能直接访问外部类的非静态成员
static class StaticNestedClass {
void accessMembers(OuterClass outer) {
// 编译错误,无法直接访问外部类的非静态成员
//IO.println(outerField);
IO.println(outer.outerField);
IO.println(staticOuterField);
}
}
public static void main(String[] args) {
IO.println("Inner class:");
IO.println("------------");
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
innerObject.accessMembers();
IO.println("\nStatic nested class:");
IO.println("----------------------");
StaticNestedClass staticNestedObject = new StaticNestedClass();
staticNestedObject.accessMembers(outerObject);
IO.println("\nTop-level class:");
IO.println("------------------");
TopLevelClass topLevelClass = new TopLevelClass();
topLevelClass.accessMembers(outerObject);
}
}
TopLevelClass.java
public class TopLevelClass {
void accessMembers(OuterClass outer) {
IO.println(outer.outerField);
IO.println(OuterClass.staticOuterField);
}
}
这个例子打印如下:
Inner class:
------------
Outer field
Static outer field
Static nested class:
----------------------
Outer field
Static outer field
Top-level class:
------------------
Outer field
Static outer field
请注意,静态嵌套类与外部类的实例成员交互的方式与任何其他顶级类相同。静态嵌套类 StaticNestedClass 无法直接访问 outerField,因为它是外部类 OuterClass 的实例变量。Java 编译器会在高亮语句处生成错误:
static class StaticNestedClass {
void accessMembers(OuterClass outer) {
// Compiler error: Cannot make a static reference to the non-static
// field outerField
IO.println(outerField);
}
}
要修复此错误,请通过对象引用访问 outerField:
IO.println(outer.outerField);
同样地,顶级类 TopLevelClass 也无法直接访问 outerField。
Shadowing(覆盖?)
Shadowing 翻译成什么比较合适?变量遮蔽?变量屏蔽?
若某类型声明(如成员变量或参数名)在特定作用域(如内部类或方法定义)中与外部作用域的声明同名,则该声明将遮蔽外部作用域的声明。仅凭名称无法直接引用被遮蔽的声明。以下示例 ShadowTest 演示了此特性:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
IO.println("x = " + x);
IO.println("this.x = " + this.x);
IO.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String[] args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
程序输出如下:
x = 23
this.x = 1
ShadowTest.this.x = 0
此示例定义了三个名为 x 的变量:ShadowTest 类的成员变量、内部类 FirstLevel 的成员变量,以及 methodInFirstLevel() 方法中的参数。作为 methodInFirstLevel() 方法参数定义的变量 x 会遮蔽内部类 FirstLevel 中的变量。因此,在 methodInFirstLevel() 方法中使用变量 x 时,它将指向方法参数。若要引用内部类 FirstLevel 的成员变量,需使用 this 关键字表示外部作用域:
IO.println("this.x = " + this.x);
访问包含更大作用域的成员变量时,需通过其所属的类名进行引用。例如,以下语句从方法 methodInFirstLevel() 中访问类 ShadowTest 的成员变量:
IO.println("ShadowTest.this.x = " + ShadowTest.this.x);
序列化
强烈不建议序列化内部类,包括局部类和匿名类。当Java编译器编译某些构造(如内部类)时,会生成合成构造;这些构造包括类、方法、字段及其他在源代码中没有对应构造的元素。合成构造使Java编译器能够在不修改JVM的情况下实现新的Java语言特性。然而,不同Java编译器实现的合成结构可能存在差异,这意味着.class文件在不同实现中也可能不同。因此,若将内部类序列化后再用其他JRE实现反序列化,可能会引发兼容性问题。
内部类的例子
要了解内部类的用法,首先考虑数组。在下面的示例中,你创建一个数组,用整数值填充它,然后按升序输出数组中偶数索引的值。
以下示例 DataStructure.java 包含:
- DataStructure 外层类包含一个构造函数,用于创建一个包含连续整数值数组(0、1、2、3 等)的 DataStructure 实例,以及一个打印数组中偶数索引值元素的方法。
- EvenIterator 内部类实现了 DataStructureIterator 接口,该接口继承自 Iterator
接口。迭代器用于遍历数据结构,通常包含用于检测最后一个元素、获取当前元素以及移至下一个元素的方法。 - 一个主要方法,该方法实例化一个 DataStructure 对象(ds),然后调用 printEven() 方法来打印数组 arrayOfInts 中具有偶数索引值的元素。
public class DataStructure {
// Create an array
private final static int SIZE = 15;
private int[] arrayOfInts = new int[SIZE];
public DataStructure() {
// Fill the array with ascending integer values
for (int i = 0; i < SIZE; i++) {
arrayOfInts[i] = i;
}
}
public void printEven() {
// Print out values of even indices of the array
DataStructureIterator iterator = this.new EvenIterator();
while (iterator.hasNext()) {
IO.print(iterator.next() + " ");
}
IO.println();
}
interface DataStructureIterator extends java.util.Iterator<Integer> {}
public class EvenIterator implements DataStructureIterator {
// Start stepping through the array from the beginning
private int nextIndex = 0;
@Override
public boolean hasNext() {
// Check if the current element is the last in the array
return (nextIndex <= SIZE - 1);
}
@Override
public Integer next() {
// Record a value of an even index of the array
Integer retValue = arrayOfInts[nextIndex];
// Get the next even element
nextIndex += 2;
return retValue;
}
}
public static void main(String[] args) {
// Fill the array with integer values and print out only values of even indices
DataStructure ds = new DataStructure();
ds.printEven();
}
}
输出如下:
0 2 4 6 8 10 12 14
请注意,EvenIterator 类直接引用了 DataStructure 对象的 arrayOfInts 实例变量。
你可以使用内部类来实现辅助类,例如本示例中所示的类。要处理用户界面事件,你必须掌握内部类的使用方法,因为事件处理机制大量依赖于它们。
局部和匿名类
还有两种额外的内部类类型。你可以在方法体内声明内部类,这类类称为局部类。你也可以在方法体内声明不命名类名的内部类,这类类称为匿名类。
修饰符
对于内部类,你可以使用与外部类其他成员相同的修饰符。例如,你可以使用访问限定符 private、public 和 protected 来限制对内部类的访问,就像你使用它们来限制对其他类成员的访问一样。
Local Classes(局部类)
局部类是在代码块中定义的类,代码块是指由大括号括起的零个或多个语句组成的集合。通常在方法主体中会定义局部类。
本节涵盖以下主题:
- 局部类的定义
- 访问外围类的成员
- 局部类的屏蔽
- 局部类与内部类的相似之处
声明一个局部类
您可以在任何代码块内定义局部类(更多信息请参阅表达式、语句和代码块)。例如,您可以在方法主体、for循环或if子句中定义局部类。
以下示例 LocalClassExample 用于验证两个电话号码。它在 validatePhoneNumber() 方法中定义了本地类 PhoneNumber:
public class LocalClassDemo {
static String regularExpression = "[^0-9]";
public static void validatePhoneNumber(String phoneNumber1, String phoneNumber2) {
final int numberLength = 10;
class PhoneNumber {
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber) {
// numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength) {
formattedPhoneNumber = currentNumber;
} else {
formattedPhoneNumber = null;
}
}
public String getNumber() {
return formattedPhoneNumber;
}
}
PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
if (myNumber1.getNumber() == null) {
IO.println("First number is invalid");
} else {
IO.println("First number is " + myNumber1.getNumber());
}
if (myNumber2.getNumber() == null) {
IO.println("Second number is invalid");
} else {
IO.println("Second number is " + myNumber2.getNumber());
}
}
public static void main(String... args) {
validatePhoneNumber("123-456-7890", "456-7890");
}
}
该示例通过以下步骤验证电话号码:首先移除电话号码中除0至9数字外的所有字符,随后检查电话号码是否恰好包含十位数字(北美电话号码的长度)。该示例输出如下内容:
First number is 1234567890
Second number is invalid
访问外围类的成员
局部类可以访问其外围类的成员。在上例中,PhoneNumber() 构造函数访问了成员 LocalClassExample.regularExpression 。
此外,局部类可以访问局部变量。但局部类仅能访问声明为 final 的局部变量。当局部类访问其外围代码块的局部变量或参数时,会捕获该变量或参数。例如,PhoneNumber() 构造函数能访问局部变量 numberLength,因为该变量声明为 final; numberLength 即为被捕获的变量。
然而,从 Java SE 8 开始,局部类可以访问其外围代码块中具有 final 或实质上 final 属性的局部变量和参数。初始化后值永不改变的变量或参数即具有实质上 final 的属性。例如,假设变量 numberLength 未声明为 final,若在 PhoneNumber() 构造函数中添加高亮显示的赋值语句,将有效电话号码的长度改为 7 位:
PhoneNumber(String phoneNumber) {
numberLength = 7;
String currentNumber = phoneNumber.replaceAll(
regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
由于这个赋值语句,变量 numberLength 实际上不再是 final 类型。因此,当内部类 PhoneNumber 尝试访问 numberLength 变量时,Java 编译器会生成类似于“从内部类引用的局部变量必须是 final 或实际上是 final”的错误信息:
if (currentNumber.length() == numberLength)
从 Java SE 8 开始,若在方法中声明局部类,该局部类即可访问方法的参数。例如,您可以在 PhoneNumber 局部类中定义以下方法:
public void printOriginalNumbers() {
IO.println("Original numbers are " + phoneNumber1 +
" and " + phoneNumber2);
}
方法 printOriginalNumbers() 访问了方法 validatePhoneNumber() 的参数 phoneNumber1 和 phoneNumber2。
局部类中声明的类型(如变量)会遮蔽外部作用域中同名的声明。更多信息请参阅遮蔽机制。
局部类与内部类的相似之处
局部类与内部类类似,因为它们无法定义或声明任何静态成员。在静态方法中定义的局部类(例如在 validatePhoneNumber() 静态方法中定义的 PhoneNumber 类)只能引用其外围类的静态成员。例如,若未将成员变量 regularExpression 声明为静态,Java 编译器将报错,提示类似于“非静态变量 regularExpression 无法从静态上下文中引用”。
局部类是非静态的,因为它们能够访问包含它们的代码块的实例成员。因此,它们不能包含大多数类型的静态声明。
你不能在代码块内部声明接口;接口本质上是静态的。例如,以下代码片段无法编译,因为接口 HelloThere 被定义在方法 greetInEnglish() 的主体内部:
public void greetInEnglish() {
interface HelloThere {
public void greet();
}
class EnglishHelloThere implements HelloThere {
public void greet() {
IO.println("Hello " + name);
}
}
HelloThere myGreeting = new EnglishHelloThere();
myGreeting.greet();
}
在局部类中不能声明静态初始化器或成员接口。以下代码片段无法编译,因为方法 EnglishGoodbye.sayGoodbye() 被声明为静态。当编译器遇到该方法定义时,会生成类似于”修饰符 static 仅允许用于常量变量声明”的错误:
本地类可以包含静态成员,前提是这些成员必须是常量变量。(常量变量是指声明为 final 并使用编译时常量表达式初始化的基本类型或 String 类型的变量。编译时常量表达式通常是字符串或可在编译时求值的算术表达式。更多信息请参阅《理解类成员》。)以下代码片段能够编译,因为静态成员 EnglishGoodbye.farewell 是常量变量:
public void sayGoodbyeInEnglish() {
class EnglishGoodbye {
public static final String farewell = "Bye bye";
public void sayGoodbye() {
IO.println(farewell);
}
}
EnglishGoodbye myEnglishGoodbye = new EnglishGoodbye();
myEnglishGoodbye.sayGoodbye();
}
局部类的使用频率远低于静态内部类和匿名内部类。
匿名类
匿名类能让你的代码更简洁。它们允许你在声明类的同时直接实例化该类。它们类似于局部类,但没有名称。当你需要仅使用局部类一次时,可以采用这种方式。
声明匿名类
局部类是类声明,而匿名类则是表达式,这意味着你需要在另一个表达式中定义该类。以下示例 HelloWorldAnonymousClasses 在局部变量 frenchGreeting 和 spanishGreeting 的初始化语句中使用了匿名类,但变量 englishGreeting 的初始化则使用了局部类:
public class HelloWorldAnonymousClasses {
interface HelloWorld {
public void greet();
public void greetSomeone(String someone);
}
public void sayHello() {
class EnglishGreeting implements HelloWorld {
String name = "world";
public void greet() {
greetSomeone("world");
}
public void greetSomeone(String someone) {
name = someone;
IO.println("Hello " + name);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
IO.println("Salut " + name);
}
};
HelloWorld spanishGreeting = new HelloWorld() {
String name = "mundo";
public void greet() {
greetSomeone("mundo");
}
public void greetSomeone(String someone) {
name = someone;
IO.println("Hola " + name);
}
};
englishGreeting.greet();
frenchGreeting.greetSomeone("Fred");
spanishGreeting.greet();
}
public static void main(String[] args) {
HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
如前所述,匿名类是一种表达式。匿名类表达式的语法类似于构造函数的调用,区别在于其中包含一段代码块定义的类。
考虑 frenchGreeting 对象的实例化过程:
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
IO.println("Salut " + name);
}
};
匿名类表达式由以下部分组成:
- new 操作符
- 要实现的接口名称或要扩展的类名称。在此示例中,匿名类实现了接口 HelloWorld 。
- 括号中包含构造函数的参数,就像普通的类实例创建表达式一样。注意:当你实现接口时,没有构造函数,因此使用一对空括号,如本例所示。
- 主体,即类声明主体。更具体地说,在主体中允许方法声明,但不允许语句。
- 由于匿名类定义是一个表达式,它必须作为语句的一部分存在。在此示例中,匿名类表达式是实例化 frenchGreeting 对象的语句组成部分。(这解释了为何闭合大括号后需要分号。)
访问外围作用域的局部变量,以及声明和访问匿名类的成员
与局部类类似,匿名类可以捕获变量;它们对包含作用域的局部变量具有相同的访问权限:
- 匿名类可以访问其外围类的成员。
- 匿名类无法访问其外围作用域中未声明为final或实质上为final的局部变量。
- 与嵌套类类似,在匿名类中声明的类型(如变量)会遮蔽外部作用域中所有同名声明。更多信息请参阅遮蔽机制。
匿名类在其成员方面也与局部类具有相同的限制:
- 在匿名类中不能声明静态初始化器或成员接口。
- 匿名类可以包含静态成员,前提是这些成员必须是常量变量。
请注意,您可以在匿名类中声明以下内容:
- 字段
- 额外方法(即使它们未实现任何超类的成员方法)
- 实例初始化器
- 局部类
然而,你无法在匿名类中声明构造函数。