类初始化顺序
类初始化顺序是 Java 基础和 JVM 之间的交叉点。很多静态变量为 null、父子类初始化异常、单例模式问题,都和初始化顺序有关。
# 1. 类初始化触发时机
类首次主动使用时会触发初始化。
常见主动使用包括:
- 创建类实例。
- 访问类的静态变量。
- 调用类的静态方法。
- 使用反射调用类。
- 初始化子类时先初始化父类。
- JVM 启动时初始化主类。
访问编译期常量通常不会触发类初始化。
public static final int VALUE = 100;
# 2. 静态初始化顺序
类内部静态变量和静态代码块按源码顺序执行。
public class Demo {
static int a = 1;
static {
a = 2;
}
static int b = a;
}
最终 a = 2,b = 2。
如果顺序变化,结果可能不同。
# 3. 父子类初始化顺序
创建子类对象时,顺序大致是:
- 父类静态变量和静态代码块。
- 子类静态变量和静态代码块。
- 父类实例变量和实例代码块。
- 父类构造方法。
- 子类实例变量和实例代码块。
- 子类构造方法。
静态初始化只执行一次,实例初始化每次创建对象都会执行。
# 4. 示例
class Parent {
static {
System.out.println("parent static");
}
{
System.out.println("parent block");
}
Parent() {
System.out.println("parent constructor");
}
}
class Child extends Parent {
static {
System.out.println("child static");
}
{
System.out.println("child block");
}
Child() {
System.out.println("child constructor");
}
}
执行:
new Child();
输出:
parent static
child static
parent block
parent constructor
child block
child constructor
# 5. 初始化陷阱
不要在父类构造方法中调用可被子类重写的方法。
class Parent {
Parent() {
init();
}
void init() {
}
}
class Child extends Parent {
private String name = "child";
@Override
void init() {
System.out.println(name);
}
}
父类构造方法执行时,子类字段可能还没有完成初始化,容易出现意外结果。
# 6. 单例模式与初始化
静态内部类单例利用类初始化的线程安全性:
public class Singleton {
private Singleton() {
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
Holder 只有在调用 getInstance() 时才初始化,JVM 保证类初始化过程线程安全。
# 7. 初始化顺序图
创建子类对象 new Child()
│
├─ 父类是否已初始化?
│ └─ 否 -> 初始化父类静态字段和静态代码块
│
├─ 子类是否已初始化?
│ └─ 否 -> 初始化子类静态字段和静态代码块
│
├─ 分配对象内存并设置零值
│
├─ 执行父类实例字段和实例代码块
│
├─ 执行父类构造方法
│
├─ 执行子类实例字段和实例代码块
│
└─ 执行子类构造方法
静态部分属于类,只执行一次;实例部分属于对象,每次 new 都执行。
# 8. 字段顺序陷阱
同一个类内部,静态字段和静态代码块按源码顺序执行:
public class Demo {
static int a = b + 1;
static int b = 10;
}
结果:
a = 1
b = 10
因为执行 a = b + 1 时,b 还只是准备阶段默认值 0。
# 9. 初始化和多态陷阱
父类构造方法中调用可重写方法是危险的:
class Parent {
Parent() {
init();
}
void init() {
}
}
class Child extends Parent {
private String name = "child";
@Override
void init() {
System.out.println(name.length());
}
}
父类构造方法执行时,子类字段可能还未初始化,name 可能是 null,导致 NPE。
# 10. 开发建议
- 静态字段初始化不要互相依赖复杂顺序。
- 静态代码块不要执行网络、数据库等外部操作。
- 构造方法中不要调用可被重写的方法。
- 单例推荐枚举或静态内部类。
- 静态初始化失败要有明确日志。
# 专家速查表
| 初始化元素 | 执行时机 | 顺序规则 |
|---|---|---|
| 静态字段赋值 | 类初始化阶段 | 按源码顺序 |
| 静态代码块 | 类初始化阶段 | 和静态字段按出现顺序交织 |
| 实例字段赋值 | 对象创建阶段 | super 构造后、本类构造前 |
| 实例代码块 | 对象创建阶段 | 和实例字段按出现顺序交织 |
| 构造方法 | 对象初始化末尾 | 先父类后子类 |
类初始化问题最常见于静态字段互相依赖、父子类构造中调用可重写方法、单例初始化顺序错误。排查时按“加载类 -> 初始化静态成员 -> 创建对象 -> 初始化实例成员 -> 构造方法”拆开看。
# 11. Tips 快问快答
Q:静态代码块一定先于构造方法执行吗?
A:是。同一个类首次初始化时,静态变量和静态代码块先执行;创建对象时再执行实例初始化和构造方法。
Q:父类静态代码块和子类静态代码块谁先执行?
A:父类先执行,子类后执行。
Q:访问 static final 常量会触发初始化吗?
A:如果是编译期常量,通常不会触发定义类初始化。
Q:类初始化是否线程安全?
A:是。JVM 保证类初始化过程同步,同一个类不会被多个线程并发执行初始化逻辑。
# 12. 总结
类初始化顺序的核心规则是:父类先于子类,静态先于实例,同级按源码顺序。理解这些规则,可以避免很多隐藏在静态变量、继承和构造方法里的问题。