Java中的数据初始化
# Java中的数据初始化
# 初始化的背景
Java 程序在运行时,会涉及 类的加载过程(Class Loading)。
当类被使用时,JVM 会按顺序执行:
加载(Loading)
→ 验证(Verification)
→ 准备(Preparation)
→ 解析(Resolution)
→ 初始化(Initialization)
2
3
4
5
其中 初始化(Initialization) 是最后一步,它的作用是:
初始化阶段:执行类中的 初始化逻辑
- 给 静态变量 赋值(包括显示赋值和静态代码块)
- 执行类的
static {}静态代码块 - 确保类处于可用状态
它让类从“已加载”状态变成“已准备好正常使用”的状态。
# 一些追问
类初始化和构造方法是什么关系?
它们的关系是:类初始化是“类级别”的动作,构造方法是“对象级别”的动作。
类初始化 → 针对类||构造方法调用 → 针对对象实例
为什么要分“类初始化”和“构造方法”两个机制?
- 因为 Java 是面向对象的,类和对象是两个层级
- 类本身需要被初始化(静态资源准备)
- 对象实例也需要被初始化(实例字段准备)
如果不分层级,会造成混乱、效率低下。
# Java中数据的初始化-顺序
Java 的数据初始化流程,我们分为 两大部分:类初始化(静态) 和 对象初始化(实例)。
这里我们先不讨论,JVM那一层对文件的读取,加载这些步骤,只关注,Java中的类和对象以及对应数据(或者属性),他们的初始化顺序以及注意点是什么?
本次,我们只关注————初始化(Initialization),这个步骤。
# 静态变量和对象初始化
# 静态数据初始化
执行静态变量的显示初始化 + 静态代码块(按代码顺序),静态变量和静态代码块的初始化顺序严格按照代码中出现的顺序
- 如果调用,那么顺序就是这样的,静态变量显式赋值 → 静态代码块 → 静态方法,但一定注意,先初始化的在前。
public class A {
static int x = initX(); // 1
static { // 2
System.out.println("static block");
}
static int y = 100; // 3
}
2
3
4
5
6
7
注意:
初始化顺序问题:如果一个静态变量依赖于另一个静态变量,必须确保被依赖的变量先声明
不能在局部变量中使用:
static不能用于方法内部的局部变量不能访问非静态成员:静态变量不能直接访问实例变量或实例方法
生命周期:与程序生命周期相同,从类加载开始到程序结束
# 对象初始化
接着是,对象初始化(Instance Initialization)
对应代码中就是,使用 new 创建对象时,如下列示例:
A a = new A();
每次创建对象时,实例的初始化,大概要经历以下几个过程:
- 父类的实例变量(默认初始化 → 显式初始化)
- 父类的实例代码块(即非静态代码块 )
- 父类的构造器
- 子类的实例变量(默认初始化 → 显式初始化)
- 子类的实例代码块
- 子类的构造器
下面是更详细的说明:
1.为对象分配内存,所有字段设为默认值
// 比如这样
int age = 18;
String name = "Tom";
2
3
2.调用父类构造器(super())
构造器第一行 隐式存在 super(),由 JVM 强制执行。
并且在调用构造方法前会执行父类的:
- 实例字段初始化
- 实例代码块
父类实例变量初始化
父类实例代码块
父类构造方法
2
3
3.初始化本类实例变量和实例代码块(按代码顺序)
比如像下面这样:
public class A {
int x = initX(); // 1
{ // 2
System.out.println("instance block");
}
int y = 10; // 3
A() { // 4
System.out.println("constructor");
}
}
2
3
4
5
6
7
8
9
10
11
# 总结
# 为什么顺序是这样?(核心原因)
Java 的初始化设计遵循 从父到子、从静态到实例、从默认值到显示赋值 的原则。
原因如下:
- 静态属于类,本质上在对象存在前就必须完成
静态成员在 JVM 的 Method Area 中,但实例成员在堆里。 只有类先初始化,才能创建对象。
- 父类决定子类的内存结构,因此必须先初始化父类
因为子类对象 包含父类部分(继承结构):
Object -> Parent -> Child
- 构造方法要使用实例字段,因此字段必须先初始化
例如构造方法可能访问字段:
this.x += 1;
如果字段还未初始化,就无法正确执行构造逻辑。
# 加载顺序
类加载阶段(一次):
-------------------------------------------
1. 静态变量默认值
2. 静态变量显示初始化
3. 静态代码块
-------------------------------------------
对象实例化阶段(每次 new):
-------------------------------------------
4. 父类实例变量默认值
5. 父类实例变量显示初始化 + 父类实例代码块
6. 父类构造方法
-------------------------------------------
7. 子类实例变量默认值
8. 子类实例变量显示初始化 + 子类实例代码块
9. 子类构造方法
-------------------------------------------
对象创建完成
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 书籍上的总结
“构造器,这种精巧的初始化机制,应该给了读者很强的暗示:初始化在Java中占有至关重要的地位。C++的发明人Bjarne Stroustrup在设计C++期间,在针对C语言的生产效率所进行的最初调查中发现,大量编程错误都源于不正确的初始化。这种错误很难发现,并且不恰当的清理也会导致类似问题。构造器能保证正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以有了完全的控制,也很安全。”
摘录来自 Java编程思想(第4版) (计算机科学丛书,Java学习必读经典,殿堂级著作!赢得了全球程序员的广泛赞誉!)
# 一些注意点
- 变量和方法
“在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。”
- 静态数据
“无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null。”
- 代码块
代码块就是显式的初始化。非静态代码块,在创建对象时执行,且在构造方法之前执行。无论 new 多少个对象,静态代码块只运行一次,在类初始化时发生。
# Java没有构造方法可以吗
Java 中的类“不能没有构造方法”,但可以“不显式写构造方法”。
如果你不写构造方法,编译器会自动帮你生成一个默认的无参构造方法。
构造方法是对象初始化的核心入口,JVM 创建对象时必须执行一个构造方法。
无论是你自己写的,还是编译器自动补的,JVM 都会在 new 的时候执行构造方法,否则对象的初始化流程无法完成。
整个过程可以理解为:
1. new 分配内存(堆上)
2. 所有成员变量设置默认值
3. 调用构造方法(显式或默认)
- 执行父类构造器
- 初始化实例字段、实例代码块
- 执行构造方法体
4. 返回引用
2
3
4
5
6
7
如果真的没有任何构造方法存在,那 JVM 就无法执行第 3 步,对象就会变成“未初始化的内存块”——这是不允许的。
Java 设计者之所以强制所有类都有构造方法,有三个重要原因:
# 保证对象状态安全
- Java 的目标之一是让对象永远处于“合法状态”。
- 构造方法是唯一能保证在对象创建时赋予有效值的入口。
没有构造方法,就可能产生“只分配了内存但没初始化”的对象,这违反了 Java 的安全模型。
# 统一对象创建流程
构造方法提供了一个固定的生命周期钩子,使所有对象的创建遵循统一的流程:
父类构造方法 → 当前类实例初始化 → 当前类构造方法
这使继承体系中的初始化顺序清晰、可预测。
# 为反射、序列化、框架机制提供钩子
Java 中大量框架(如 Spring、JPA、Jackson)都会通过反射调用构造方法来创建对象。
例如:
User user = User.class.getDeclaredConstructor().newInstance();
如果类没有构造方法(即使空的),这些框架都无法实例化对象。
# 一个疑问
没有构造方法,就可能产生“只分配了内存但没初始化”的对象,这违反了 Java 的安全模型。那假如我们设计一个方法,但不是构造方法,每当我们调用对象的时候,开发者手动调用它以下不就行了吗
1.JVM 层面规定:对象创建流程不可绕过构造方法
当你执行:
User user = new User();
JVM 实际执行的字节码 roughly 是:
new User // 分配内存(堆上)
dup // 复制栈顶引用
invokespecial <init> // 调用构造方法 User.<init>()
2
3
invokespecial <init> 这一行是强制执行的。
构造方法 <init> 是 JVM 级别定义的初始化入口。
没有它,对象就无法从 “未定义” 变成 “已初始化”。
如果你去掉构造方法,JVM 根本不会允许你 new 这个类。
2.“init()” 不是强制调用的 → 容易出错
假如我们自己写:
User u = new User();
u.init("Tom", 18);
2
那开发者完全可能忘记调用 init():
User u = new User(); // 忘了 init()
u.doSomething(); // 空指针风险、逻辑错误
2
此时对象在内存上虽然存在,但它的状态是非法的。 这与 Java 的基本设计理念冲突 ——
“对象在创建完毕后,必须是可用的”。
也就是说:
- 构造方法 = 强制初始化
- 普通方法 = 可选调用
安全性完全不同。
- 构造方法参与继承体系的“初始化链条”
在继承体系中,Java 会保证:
“父类先初始化 → 再初始化子类”。
例子:
class Parent {
Parent() { System.out.println("Parent init"); }
}
class Child extends Parent {
Child() { System.out.println("Child init"); }
}
new Child();
2
3
4
5
6
7
8
9
10
如果没有构造方法,子类根本无法保证父类的初始化逻辑被执行。 而 Java 的类加载和对象创建依赖于这种层层构造链。
构造方法的特点是:
- 在
new的时候 自动执行;- 执行顺序固定;
- 可以强制父类 → 子类依次初始化。
这样可以保证:
“只要对象能被创建出来,它就是一个状态完整、可用的对象。”
这就是 Java 所说的对象安全模型(Object Safety Model)。
普通方法(例如 init())无法自动触发父类初始化。
4.框架 & 反射机制都依赖构造方法存在
Java 的框架(Spring、Hibernate、Jackson、JPA、反射 API) 在创建对象时几乎都调用构造方法,例如:
User u = User.class.getDeclaredConstructor().newInstance();
如果你的类没有构造方法或构造方法不可访问,这些框架就无法实例化你的类。
# 总结
构造方法的存在,是为了让对象的“创建”和“初始化”成为一个原子、受控、可靠的过程。
也就是说:
对象从“只是堆内存上的一块空间” → “可安全使用的实例”
必须经过一条由 JVM 保证的、不可跳过的路径。
额外补充一个隐藏的原因(很多人忽略)
构造方法还能确保对象内存布局在 JVM 层面是完整可见的。
在 Java 内存模型(JMM)中,构造方法执行完毕后, 对象的字段才对其他线程安全可见。 如果你靠
init()手动赋值,而没有构造方法的写屏障(memory barrier), 那么在多线程环境下,对象可能被其他线程看到“部分初始化”的状态。