构造方法不是对象初始化的唯一入口,JVM在调用前已分配内存、设默认值、执行父类构造链;字段初始化、实例块在构造体前执行,且存在绕过构造方法创建对象的方式。

构造方法不是“初始化的唯一入口”
很多初学者误以为 new MyClass() 之后,代码一定会从构造方法第一行开始执行。实际上,JVM 在调用构造方法前,已隐式完成几件事:分配内存、将所有字段置为默认值(0、null、false)、执行父类构造链(直到 Object)。构造方法只是你“能插入自定义逻辑”的最早可控节点,不是对象生命周期的起点。
这意味着:
- 字段声明时的初始化表达式(如
private List)会在构造方法体执行前完成,但顺序在父类构造调用之后、本类构造体之前;list = new ArrayList(); - 如果父类构造抛异常,你的构造方法体根本不会执行;
- 使用
Unsafe.allocateInstance()或反序列化(如ObjectInputStream)可绕过构造方法创建对象——此时字段全为默认值,且无任何初始化逻辑运行。
构造方法链中 this(...) 和 super(...) 的限制
Java 强制要求每个构造方法的第一条语句必须是显式或隐式的构造调用:this(...)(本类其他构造)或 super(...)(父类构造)。没写?编译器自动补 super();。但这两者不能共存,也不能出现在条件分支里。
常见错误包括:
立即学习“Java免费学习笔记(深入)”;
- 在
if块内写this(...)→ 编译报错:call to this must be first statement; - 父类没有无参构造,而子类构造未显式调用
super(...)→ 编译失败:constructor Parent() is undefined; - 递归调用
this(...)(比如 A 调 B,B 又调 A)→ 编译期就拒绝,不等运行。
本质是 JVM 需要明确构造路径的拓扑顺序,确保字段初始化和继承链可控。
实例初始化块比构造方法体更早执行
Java 允许用 { ... } 定义实例初始化块(instance initializer block),它会被编译器“复制”到每个构造方法体的开头(在显式 super(...) 或 this(...) 之后、其余代码之前)。
public class Example {
private int a = 1; // 字段初始化
{ System.out.println("init block"); } // 实例初始化块
public Example() {
System.out.println("ctor body");
}
}
执行 new Example() 输出顺序是:
-
init block(因为字段初始化后、构造体前) ctor body
这个机制常被用于:避免多个构造方法中重复写相同初始化逻辑;或在匿名内部类/lambda 捕获外部变量受限时,做轻量预处理。但注意:它无法接收参数,也无法抛受检异常(除非用 try-catch 包裹)。
静态字段与静态初始化块只执行一次,且早于任何构造
静态成员属于类,不属于对象。JVM 在首次主动使用该类(如 new、调用静态方法、访问静态字段)时触发类初始化,此时按源码顺序执行:
- 静态字段的初始化表达式(如
static int x = calc();) - 静态初始化块(
static { ... })
这个过程与构造方法完全解耦。哪怕你 never new 一个对象,只要引用了某个静态字段,类初始化就发生;反之,new 十万个对象,静态部分也只执行一次。
容易忽略的点:
- 子类引用父类静态成员,不会触发子类初始化(只触发父类);
- 数组类型(如
Example[])不会触发Example类初始化; - 常量(
static final int X = 42;)在编译期就内联,不触发类初始化。
对象初始化真正复杂的地方,不在语法糖,而在这些不同阶段(类加载、内存分配、字段默认值、静态初始化、实例初始化、构造体)的交织顺序和可见性规则。稍有不慎,多线程下就可能看到未完全初始化的对象状态。










