Vasily Strelnikov 2023-04-27
Java 可以使用实例初始化块(instance initializer blocks)在对象创建期间对字段进行初始化。
初始化器可用于为对象和类中的字段设置初始值。Java 中有三种类型的初始化器:
- 字段初始化表达式(Field initializer expressions)
- 静态初始化块(Static initializer blocks)
- 实例初始化块(Instance initializer blocks)
字段的初始化可以在字段声明语句中通过初始化表达式指定。初始化表达式的值必须与所声明字段的类型兼容(即满足赋值兼容性)。
Java 允许在类中定义静态初始化块。尽管这类代码块可以包含任意代码,但它们主要用于初始化静态字段。静态初始化块中的代码仅在类被加载并初始化时执行一次。
正如静态初始化块可用于初始化命名类中的静态字段一样,Java 也提供了实例初始化块,用于在对象创建期间初始化字段——这正是本文的主题。
在这方面,实例初始化块在对象创建过程中起到的作用与构造函数相同。其实例初始化块的语法与局部代码块相同,如下列代码第 (2) 行所示。每当创建该类的一个实例时,局部代码块中的代码都会被执行。
class InstanceInitializers {
long[] squares = new long[10]; // (1)
// ...
{ // (2) 实例初始化块
for (int i = 0; i < squares.length; i++)
squares[i] = i * i;
}
// ...
}
在上述代码中,首先在第 (1) 行创建了指定长度的数组 squares;随后,每次创建 InstanceInitializers 类的实例时,都会执行第 (2) 行的实例初始化块。注意:实例初始化块并不包含在任何方法中。一个类可以拥有多个实例初始化块,这些块(以及实例字段声明中的初始化表达式)会按照它们在类中出现的顺序依次执行。
实例初始化块的声明顺序
与其他初始化器类似,实例初始化块不能通过字段的简单名称(simple name)在读取操作中向前引用尚未声明的字段,因为这会违反“先声明后使用”的规则。然而,使用 this 关键字访问字段则不会有问题。
清单 1 中的类在第 (1) 行定义了一个实例初始化块,其中包含了对字段 i、j 和 k 的向前引用,而这些字段分别在第 (7)、(8) 和 (9) 行声明。在第 (3)、(4)、(5) 和 (6) 行,这些字段通过 this 引用进行读取操作。如果在这些行使用字段的简单名称来读取其值,则会违反“先声明后使用”规则,导致编译错误——无论这些字段是否带有初始化表达式,或者是否被声明为 final。
在第 (2) 行,字段 i 和 j 被用于写入操作,这种情况下使用简单名称是允许的。但需注意确保字段被正确初始化。在第 (3)、(4) 和 (5) 行,i 和 j 的值为 10。然而,当执行到实例字段声明处的初始化表达式时,j 的值会被重新设为 100。
清单 1. 使用 this 关键字访问字段
public class InstanceInitializersII {
{ // 包含向前引用的实例初始化块 (1)
i = j = 10; // (2) 允许。
int result = this.i * this.j; // (3) i 是 10,j 是 10。
System.out.println(this.i); // (4) 10
System.out.println(this.j); // (5) 10
System.out.println(this.k); // (6) 50
}
// 实例字段声明
int i; // (7) 无初始化表达式的字段声明
int j = 100; // (8) 带初始化表达式的字段声明
final int k = 50; // (9) 带常量表达式的 final 实例字段
}
清单 2 进一步说明了实例初始化块中的一些细微之处。第 (4) 行的代码试图在字段 nsf1 声明之前读取其值,构成了非法的向前引用。而第 (11) 行的读取操作发生在字段声明之后,因此是合法的。赋值操作符左侧的向前引用始终是允许的,如第 (2)、(3)、(5) 和 (7) 行所示。此外,第 (5) 和 (12) 行展示了如何在实例初始化块中使用 var 声明局部变量。
清单 2. 实例初始化块与向前引用
public class NonStaticForwardReferences {
{ // (1) 实例初始化块
nsf1 = 10; // (2) OK。允许对 nsf1 赋值。
nsf1 = sf1; // (3) OK。非静态上下文中访问静态字段。
// int a = 2 * nsf1; // (4) 不允许。在声明前读取字段。
var b = nsf1 = 20; // (5) OK。允许对 nsf1 赋值。
int c = this.nsf1; // (6) OK。未使用简单名称访问。
}
int nsf1 = nsf2 = 30; // (7) 非静态字段。允许对 nsf2 赋值。
int nsf2; // (8) 非静态字段。
static int sf1 = 5; // (9) 静态字段。
{ // (10) 实例初始化块
int d = 2 * nsf1; // (11) OK。声明后读取。
var e = nsf1 = 50; // (12) OK。允许对 nsf1 赋值。
}
public static void main(String[] args) {
NonStaticForwardReferences objRef = new NonStaticForwardReferences();
System.out.println("nsf1: " + objRef.nsf1);
System.out.println("nsf2: " + objRef.nsf2);
}
}
上述代码的输出为:
nsf1: 50
nsf2: 30
与字段初始化表达式一样,在实例初始化块中也可以使用关键字 this 和 super 来引用当前对象。(注意:实例初始化块中不允许使用 return 语句。)
实例初始化块可用于提取所有构造函数都需要执行的公共初始化代码。一个典型应用场景是在匿名类中——匿名类不能声明构造函数,但可以使用实例初始化块来初始化字段。在清单 3 中,第 (1) 行定义的匿名类使用第 (2) 行的实例初始化块对其字段进行了初始化。
清单 3. 匿名类中的实例初始化块
// 文件: InstanceInitBlock.java
class Base {
protected int a;
protected int b;
void print() { System.out.println("a: " + a); }
}
class AnonymousClassMaker {
Base createAnonymous() {
return new Base() { // (1) 匿名类
{ // (2) 实例初始化块
a = 5; b = 10;
}
@Override
void print() {
super.print();
System.out.println("b: " + b);
}
}; // 匿名类结束
}
}
public class InstanceInitBlock {
public static void main(String[] args) {
new AnonymousClassMaker().createAnonymous().print();
}
}
上述代码的输出为:
a: 5
b: 10
实例初始化块中的异常处理
实例初始化块中的异常处理方式与静态初始化块类似,但也存在一个重要区别:
实例初始化块中可以抛出未捕获的受检异常(checked exception),前提是该异常已在类中每个构造函数的
throws子句中声明。
这是因为对象的创建总是通过某个构造函数完成的。而静态初始化块则无法做到这一点,因为在类初始化过程中不涉及构造函数。
对于匿名类中的实例初始化块,限制更少:它们可以抛出任意类型的异常(包括受检异常),无需显式声明。