在 Java 中,一个类的加载、链接、初始化过程由 JVM 严格按照规范执行,而类内部各元素(静态成员、实例成员、构造器等)的初始化顺序也有明确规则。理解这一顺序对掌握对象创建机制、避免初始化错误(如空指针、未定义行为)至关重要。
一、整体流程:类的生命周期(JVM 层面)
Java 类的生命周期分为以下阶段:
-
加载(Loading)
- 通过类加载器(ClassLoader)将
.class
字节码文件读入内存; - 生成
java.lang.Class
对象。
- 通过类加载器(ClassLoader)将
-
链接(Linking)
- 验证(Verification):确保字节码安全、合法;
- 准备(Preparation):为静态变量分配内存并设置默认初始值(如
0
、null
、false
),不执行初始化代码; - 解析(Resolution):将符号引用转为直接引用(可选,部分在初始化后进行)。
-
初始化(Initialization)
- 执行静态初始化代码:包括静态变量显式赋值和静态代码块(
static {}
); - 这是类初始化阶段,也是我们关注“初始化顺序”的核心。
- 执行静态初始化代码:包括静态变量显式赋值和静态代码块(
✅ 触发类初始化的时机(JLS §12.4.1):
- 创建类的实例(
new
);- 访问类的静态非常量字段(
static final
基本类型/字符串字面量除外);- 调用类的静态方法;
- 使用反射(如
Class.forName()
);- 初始化子类时,先初始化父类;
- 启动类(含
main
方法的类)。
二、类内部元素的初始化顺序(重点)
当一个类被初始化(或创建对象)时,其内部元素按以下严格顺序执行:
📌 1. 父类优先原则
先初始化父类,再初始化子类(递归至
Object
)。
📌 2. 静态成员先于实例成员
静态内容只初始化一次(类加载时),实例内容每次创建对象时初始化。
📌 3. 同类中按代码书写顺序执行
✅ 完整初始化顺序(创建对象时)
假设有一个子类 Child
继承自 Parent
,两者均包含各类成员:
1. 父类 Parent 的静态变量(默认值 → 显式赋值)
2. 父类 Parent 的静态代码块(static {})
3. 子类 Child 的静态变量(默认值 → 显式赋值)
4. 子类 Child 的静态代码块(static {})
↓
5. 父类 Parent 的实例变量(默认值 → 显式赋值)
6. 父类 Parent 的实例代码块({})
7. 父类 Parent 的构造器
8. 子类 Child 的实例变量(默认值 → 显式赋值)
9. 子类 Child 的实例代码块({})
10. 子类 Child 的构造器
💡 关键点:
- 静态内容在类首次主动使用时初始化一次;
- 实例内容在每次 new 对象时执行;
- 变量赋值与代码块按源码从上到下顺序执行。
三、详细示例与中文注释说明
示例代码:展示完整初始化顺序
// 父类
class Parent {
// 1. 静态变量(准备阶段设为 null,初始化阶段赋值)
static String parentStaticField = initParentStaticField();
// 2. 静态代码块
static {
System.out.println("【父类】静态代码块执行");
}
// 5. 实例变量
String parentInstanceField = initParentInstanceField();
// 6. 实例代码块
{
System.out.println("【父类】实例代码块执行");
}
// 7. 构造器
public Parent() {
System.out.println("【父类】构造器执行");
}
// 辅助方法(用于观察初始化时机)
static String initParentStaticField() {
System.out.println("【父类】静态变量初始化");
return "Parent Static";
}
String initParentInstanceField() {
System.out.println("【父类】实例变量初始化");
return "Parent Instance";
}
}
// 子类
class Child extends Parent {
// 3. 静态变量
static String childStaticField = initChildStaticField();
// 4. 静态代码块
static {
System.out.println("【子类】静态代码块执行");
}
// 8. 实例变量
String childInstanceField = initChildInstanceField();
// 9. 实例代码块
{
System.out.println("【子类】实例代码块执行");
}
// 10. 构造器
public Child() {
System.out.println("【子类】构造器执行");
}
static String initChildStaticField() {
System.out.println("【子类】静态变量初始化");
return "Child Static";
}
String initChildInstanceField() {
System.out.println("【子类】实例变量初始化");
return "Child Instance";
}
}
// 测试类
public class InitializationOrderDemo {
public static void main(String[] args) {
System.out.println("=== 第一次创建 Child 对象 ===");
new Child();
System.out.println("\n=== 第二次创建 Child 对象 ===");
new Child(); // 静态部分不会重复执行
}
}
🔍 输出结果分析:
=== 第一次创建 Child 对象 ===
【父类】静态变量初始化
【父类】静态代码块执行
【子类】静态变量初始化
【子类】静态代码块执行
【父类】实例变量初始化
【父类】实例代码块执行
【父类】构造器执行
【子类】实例变量初始化
【子类】实例代码块执行
【子类】构造器执行
=== 第二次创建 Child 对象 ===
【父类】实例变量初始化
【父类】实例代码块执行
【父类】构造器执行
【子类】实例变量初始化
【子类】实例代码块执行
【子类】构造器执行
✅ 结论:
- 静态部分(1-4)仅在首次使用类时执行一次;
- 实例部分(5-10)每次 new 对象都执行;
- 父类始终先于子类初始化。
四、特殊情况说明
4.1 静态常量(static final
)不触发类初始化
class Constants {
public static final int VALUE = 100; // 编译期常量
static {
System.out.println("Constants 类被初始化!");
}
}
public class ConstantDemo {
public static void main(String[] args) {
System.out.println(Constants.VALUE); // 直接输出 100,不触发初始化!
}
}
输出:
100
(无“Constants 类被初始化!”)
原因:VALUE
是编译期常量,值已内联到调用处,无需加载Constants
类。
4.2 静态变量赋值 vs 静态代码块
class OrderTest {
static int a = 1;
static {
a = 2;
}
static int b = a; // b = 2
public static void main(String[] args) {
System.out.println("a=" + a + ", b=" + b); // a=2, b=2
}
}
说明:静态变量和静态代码块按源码顺序执行。
4.3 构造器中的 super()
调用
- 子类构造器第一行隐式或显式调用
super()
; - 因此父类构造器总是在子类构造器之前执行。
五、总结:初始化顺序口诀
“父静子静,父实子实;
变量代码块,顺序不能乱;
构造最后来,super 在最前。”
阶段 | 元素 | 执行时机 | 次数 |
---|---|---|---|
静态初始化 | 父类静态变量/代码块 | 类首次主动使用时 | 1 次 |
子类静态变量/代码块 | 同上 | 1 次 | |
实例初始化 | 父类实例变量/代码块 | 每次 new 对象时 | 每次 |
父类构造器 | 同上 | 每次 | |
子类实例变量/代码块 | 同上 | 每次 | |
子类构造器 | 同上 | 每次 |
六、实际应用建议
- 避免在静态初始化中依赖未初始化的静态变量;
- 不要在父类构造器中调用可能被子类重写的方法(此时子类尚未初始化,可能导致空指针);
- 使用
final
字段确保初始化完成后不可变; - 理解类初始化时机,避免
ClassNotFoundException
或NoClassDefFoundError
。
掌握类初始化顺序,是深入理解 Java 对象模型、JVM 机制和编写健壮代码的基础。