tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

    • Java中的一些概念
    • Java中的数组
    • String类跟关键字final
    • Java 日期时间与Date
    • Java中的异常处理
    • Java枚举
    • Java序列化和反序列化
    • Java中的注解
    • Java中的IO流
    • Java中的数据初始化
      • 初始化的背景
        • 一些追问
      • Java中数据的初始化-顺序
        • 静态变量和对象初始化
        • 静态数据初始化
        • 对象初始化
        • 总结
        • 为什么顺序是这样?(核心原因)
        • 加载顺序
        • 书籍上的总结
        • 一些注意点
        • Java没有构造方法可以吗
        • 保证对象状态安全
        • 统一对象创建流程
        • 为反射、序列化、框架机制提供钩子
        • 一个疑问
        • 总结
    • Java中抽象类与接口
  • 高级进阶

  • 并发合集

  • JVM合集

  • 实战细节与其他

  • 代码之丑与提升

  • 《Java》学习笔记
  • Java基础与面向对象
EffectTang
2025-11-09
目录

Java中的数据初始化

# Java中的数据初始化

# 初始化的背景

Java 程序在运行时,会涉及 类的加载过程(Class Loading)。

当类被使用时,JVM 会按顺序执行:

加载(Loading)
→ 验证(Verification)
→ 准备(Preparation)
→ 解析(Resolution)
→ 初始化(Initialization)
1
2
3
4
5

其中 初始化(Initialization) 是最后一步,它的作用是:

初始化阶段:执行类中的 初始化逻辑

  • 给 静态变量 赋值(包括显示赋值和静态代码块)
  • 执行类的 static {} 静态代码块
  • 确保类处于可用状态

它让类从“已加载”状态变成“已准备好正常使用”的状态。

# 一些追问

类初始化和构造方法是什么关系?

它们的关系是:类初始化是“类级别”的动作,构造方法是“对象级别”的动作。

类初始化 → 针对类||构造方法调用 → 针对对象实例

为什么要分“类初始化”和“构造方法”两个机制?

  1. 因为 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
}
1
2
3
4
5
6
7

注意:

  • 初始化顺序问题:如果一个静态变量依赖于另一个静态变量,必须确保被依赖的变量先声明

  • 不能在局部变量中使用:static不能用于方法内部的局部变量

  • 不能访问非静态成员:静态变量不能直接访问实例变量或实例方法

  • 生命周期:与程序生命周期相同,从类加载开始到程序结束

# 对象初始化

接着是,对象初始化(Instance Initialization)

对应代码中就是,使用 new 创建对象时,如下列示例:

A a = new A();
1

每次创建对象时,实例的初始化,大概要经历以下几个过程:

  1. 父类的实例变量(默认初始化 → 显式初始化)
  2. 父类的实例代码块(即非静态代码块 )
  3. 父类的构造器
  4. 子类的实例变量(默认初始化 → 显式初始化)
  5. 子类的实例代码块
  6. 子类的构造器

下面是更详细的说明:

1.为对象分配内存,所有字段设为默认值

// 比如这样
int age = 18;
String name = "Tom";
1
2
3

2.调用父类构造器(super())

构造器第一行 隐式存在 super(),由 JVM 强制执行。

并且在调用构造方法前会执行父类的:

  • 实例字段初始化
  • 实例代码块
父类实例变量初始化
父类实例代码块
父类构造方法
1
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");
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 总结

# 为什么顺序是这样?(核心原因)

Java 的初始化设计遵循 从父到子、从静态到实例、从默认值到显示赋值 的原则。

原因如下:

  • 静态属于类,本质上在对象存在前就必须完成

静态成员在 JVM 的 Method Area 中,但实例成员在堆里。 只有类先初始化,才能创建对象。

  • 父类决定子类的内存结构,因此必须先初始化父类

因为子类对象 包含父类部分(继承结构):

Object -> Parent -> Child
1
  • 构造方法要使用实例字段,因此字段必须先初始化

例如构造方法可能访问字段:

this.x += 1;
1

如果字段还未初始化,就无法正确执行构造逻辑。

# 加载顺序

类加载阶段(一次):
-------------------------------------------
1. 静态变量默认值
2. 静态变量显示初始化
3. 静态代码块
-------------------------------------------

对象实例化阶段(每次 new):
-------------------------------------------
4. 父类实例变量默认值
5. 父类实例变量显示初始化 + 父类实例代码块
6. 父类构造方法
-------------------------------------------
7. 子类实例变量默认值
8. 子类实例变量显示初始化 + 子类实例代码块
9. 子类构造方法
-------------------------------------------
对象创建完成

1
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. 返回引用
1
2
3
4
5
6
7

如果真的没有任何构造方法存在,那 JVM 就无法执行第 3 步,对象就会变成“未初始化的内存块”——这是不允许的。

Java 设计者之所以强制所有类都有构造方法,有三个重要原因:

# 保证对象状态安全

  • Java 的目标之一是让对象永远处于“合法状态”。
  • 构造方法是唯一能保证在对象创建时赋予有效值的入口。

没有构造方法,就可能产生“只分配了内存但没初始化”的对象,这违反了 Java 的安全模型。

# 统一对象创建流程

构造方法提供了一个固定的生命周期钩子,使所有对象的创建遵循统一的流程:

父类构造方法 → 当前类实例初始化 → 当前类构造方法
1

这使继承体系中的初始化顺序清晰、可预测。

# 为反射、序列化、框架机制提供钩子

Java 中大量框架(如 Spring、JPA、Jackson)都会通过反射调用构造方法来创建对象。

例如:

User user = User.class.getDeclaredConstructor().newInstance();
1

如果类没有构造方法(即使空的),这些框架都无法实例化对象。

# 一个疑问

没有构造方法,就可能产生“只分配了内存但没初始化”的对象,这违反了 Java 的安全模型。那假如我们设计一个方法,但不是构造方法,每当我们调用对象的时候,开发者手动调用它以下不就行了吗

1.JVM 层面规定:对象创建流程不可绕过构造方法

当你执行:

User user = new User();
1

JVM 实际执行的字节码 roughly 是:

new User        // 分配内存(堆上)
dup              // 复制栈顶引用
invokespecial <init> // 调用构造方法 User.<init>()
1
2
3

invokespecial <init> 这一行是强制执行的。 构造方法 <init> 是 JVM 级别定义的初始化入口。 没有它,对象就无法从 “未定义” 变成 “已初始化”。

如果你去掉构造方法,JVM 根本不会允许你 new 这个类。

2.“init()” 不是强制调用的 → 容易出错

假如我们自己写:

User u = new User();
u.init("Tom", 18);
1
2

那开发者完全可能忘记调用 init():

User u = new User();  // 忘了 init()
u.doSomething();      // 空指针风险、逻辑错误
1
2

此时对象在内存上虽然存在,但它的状态是非法的。 这与 Java 的基本设计理念冲突 ——

“对象在创建完毕后,必须是可用的”。

也就是说:

  • 构造方法 = 强制初始化
  • 普通方法 = 可选调用

安全性完全不同。

  1. 构造方法参与继承体系的“初始化链条”

在继承体系中,Java 会保证:

“父类先初始化 → 再初始化子类”。

例子:

class Parent {
    Parent() { System.out.println("Parent init"); }
}

class Child extends Parent {
    Child() { System.out.println("Child init"); }
}

new Child();

1
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();
1

如果你的类没有构造方法或构造方法不可访问,这些框架就无法实例化你的类。

# 总结

构造方法的存在,是为了让对象的“创建”和“初始化”成为一个原子、受控、可靠的过程。

也就是说:

对象从“只是堆内存上的一块空间” → “可安全使用的实例”

必须经过一条由 JVM 保证的、不可跳过的路径。


额外补充一个隐藏的原因(很多人忽略)

构造方法还能确保对象内存布局在 JVM 层面是完整可见的。

在 Java 内存模型(JMM)中,构造方法执行完毕后, 对象的字段才对其他线程安全可见。 如果你靠 init() 手动赋值,而没有构造方法的写屏障(memory barrier), 那么在多线程环境下,对象可能被其他线程看到“部分初始化”的状态。

上次更新: 2025/11/13, 15:54:01
Java中的IO流
Java中抽象类与接口

← Java中的IO流 Java中抽象类与接口→

最近更新
01
DevOps-jenkins的安装
11-23
02
Java中的垃圾回收
11-02
03
GitFlow的使用和注意
09-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式