1. Java 中对象的初始化顺序:谁先,谁后?
在 Java 中创建对象并不只是“分配内存并把值写进去”。这是一个有严格步骤的“仪式”。如果把初始化过程比作火箭发射,那么每一级都有其精确的位置。
当你写出:
Person bob = new Person("Bob", 30);
Java 会执行以下步骤:
- 为对象分配内存,并为所有字段赋予默认值。
- 执行字段的显式初始化(如果你在声明时直接赋了值)。
- 执行所有实例(非静态)初始化块——按照它们在类中书写的顺序。
- 执行你通过 new 调用的构造函数的函数体。
下面逐步展开。
2. 字段初始化
默认值
当对象刚被创建(在你显式赋值之前),它的所有字段都会获得默认值:
| 字段类型 | 默认值 |
|---|---|
|
|
|
|
|
|
|
'\u0000'(空字符) |
| 引用类型(String、其他对象) | |
显式初始化
如果你在声明字段时就赋值,这个值会在默认值之后、但在构造函数之前被设置。
public class Person {
private String name = "未命名";
private int age = 18;
}
如果你通过无参构造创建对象,这些值会保留;如果通过带参数的构造函数——它们很可能会被覆盖。
3. 初始化块:用途与工作方式
在 Java 中可以声明所谓的实例(非静态)初始化块。它们在每次创建对象时执行,发生在字段显式初始化之后,但早于构造函数。
语法:
public class Person {
{
System.out.println("正在执行实例初始化块!");
}
}
如果类中有多个这样的块,它们会按书写顺序执行。
多个块的示例
public class Person {
private String name = "未命名";
{
System.out.println("块 1:name = " + name);
name = "谜";
}
{
System.out.println("块 2:name = " + name);
}
public Person() {
System.out.println("构造函数:name = " + name);
}
}
如果创建对象:
Person p = new Person();
输出为:
块 1:name = 未命名
块 2:name = 谜
构造函数:name = 谜
初始化块有什么用?
在实际开发中很少用。通常能在块里做的事,要么可以在字段声明处完成,要么可以放在构造函数里。但有时(例如当你有多个构造函数,而部分初始化逻辑需要在所有构造函数中共享)初始化块会很方便。
4. 构造函数:初始化的最后一棒
当所有字段都拿到值(默认或显式),并且所有初始化块都执行完之后,会调用构造函数——也就是你在 new 之后指定的那个。
在构造函数中,你通常会设置字段的最终值、接收参数,并执行依赖输入数据的其他初始化。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
System.out.println("构造函数:name = " + name + ", age = " + age);
this.name = name;
this.age = age;
}
}
5. 完整初始化顺序演示
让我们写一个类,清楚展示初始化发生的顺序。
public class Person {
private String name = "未命名";
private int age = 18;
{
System.out.println("初始化块:name = " + name + ", age = " + age);
age = 21;
}
public Person() {
System.out.println("无参数构造函数:name = " + name + ", age = " + age);
}
public Person(String name, int age) {
System.out.println("带参数的构造函数:name = " + name + ", age = " + age);
this.name = name;
this.age = age;
}
public void printInfo() {
System.out.println("Person: name = " + name + ", age = " + age);
}
}
现在在我们的主类中(例如 Main):
public class Main {
public static void main(String[] args) {
System.out.println("创建对象 p1:");
Person p1 = new Person();
p1.printInfo();
System.out.println("\n创建对象 p2:");
Person p2 = new Person("John", 30);
p2.printInfo();
}
}
输出大致如下:
创建对象 p1:
初始化块:name = 未命名, age = 18
无参数构造函数:name = 未命名, age = 21
Person: name = 未命名, age = 21
创建对象 p2:
初始化块:name = 未命名, age = 18
带参数的构造函数:name = 未命名, age = 21
Person: name = John, age = 30
请注意:在带参数的构造函数中,字段值仍然是旧的(即初始化块之后的那些);而在构造函数结束之后,字段才会得到最终的值。
6. 初始化顺序图
这里有一张小的(流程图)用于可视化:
flowchart TD
A[分配内存,赋默认值] --> B[字段的显式初始化]
B --> C[执行实例初始化块]
C --> D[执行构造函数]
D --> E[对象已可使用]
7. 特点与细节
静态字段和静态块
本文只讨论实例(非静态)字段和块。静态字段和静态块在类加载时只初始化一次,而不是每次创建对象时。关于它们的更多内容,我们会在与封装相关的主题中讨论。
什么时候会初始化?
- 字段——每次创建对象时。
- 初始化块——每次创建对象时。
- 静态字段和静态块——仅在类加载时初始化一次。
能否在初始化块中访问字段?
可以!但要注意,如果字段声明在该块之后,它已经存在;而如果你在它的显式初始化之前访问它,你会得到默认值。
8. 对象初始化中的常见错误
错误 №1:以为字段会在声明之前就被初始化。
在 Java 中,类中字段和初始化块的声明顺序是有意义的。如果你在初始化块中访问一个声明在其下方的字段,它虽然已经存在,但还没有被显式初始化——得到的将是默认值。
错误 №2:在块和构造函数中重复初始化。
新手常常在块和构造函数里重复相同的逻辑。更好的做法是:要么用初始化块,要么用构造函数,或者通过 this(...) 让一个构造函数调用另一个构造函数。
错误 №3:以为静态字段会在每次创建对象时初始化。
静态字段和块的工作方式不同——它们只会在类加载时初始化一次。
错误 №4:使用未初始化的引用字段。
如果你忘了显式初始化某个引用字段(例如,String name;),那么在你在初始化块或构造函数中显式赋值之前,它的值将是 null。
GO TO FULL VERSION