类的初始化顺序是怎样的?静态代码块、构造器、父子类谁先执行?
简化版
单个类内部的初始化顺序是:静态变量/静态代码块(只在类加载时执行一次)→ 实例变量/实例代码块 → 构造器。有父子继承时,规则是「静态优先、父类优先」:先执行父类静态 → 子类静态(这两个只一次)→ 父类实例代码块和构造器 → 子类实例代码块和构造器。记忆口诀:静态早于实例、父类早于子类、变量/代码块按书写顺序。核心原因是:静态成员属于类、随类加载一次性初始化;创建对象时必须先构造好父类「地基」,才能构造子类。
详细版
父子类完整初始化顺序(new 一个子类对象时):
1. 父类静态变量 + 父类静态代码块 ┐ 类加载阶段,
2. 子类静态变量 + 子类静态代码块 ┘ 整个程序只执行一次
------ 以下每次 new 都执行 ------
3. 父类实例变量 + 父类实例代码块
4. 父类构造器
5. 子类实例变量 + 子类实例代码块
6. 子类构造器
class Parent {
static { System.out.println("1 父静态"); }
{ System.out.println("3 父实例块"); }
Parent() { System.out.println("4 父构造"); }
}
class Child extends Parent {
static { System.out.println("2 子静态"); }
{ System.out.println("5 子实例块"); }
Child() { System.out.println("6 子构造"); }
}
new Child();
// 输出:1 父静态 → 2 子静态 → 3 父实例块 → 4 父构造 → 5 子实例块 → 6 子构造
new Child();
// 再 new 一次:3→4→5→6(静态部分不再执行)
两条核心规则:
- 静态部分只执行一次(类加载时),实例部分每次 new 都执行;
- 同一类内,静态变量与静态代码块、实例变量与实例代码块,都按「书写先后顺序」执行。
⚠️ 「实例变量 + 实例代码块」是在父类构造器之后、子类构造器体之前执行的——本质是子类构造器第一行隐含
super(),super 返回后先跑子类的实例变量/实例块初始化,再执行子类构造器剩下的代码。
完整版教学
一、先分清「类加载」和「对象创建」两个阶段
理解初始化顺序,先分清两个层次的初始化:
类加载阶段(每个类一生只有一次):
初始化静态变量、执行静态代码块
触发时机:首次主动使用这个类(new、访问静态成员、反射等)
对象创建阶段(每次 new 都发生):
初始化实例变量、执行实例代码块、执行构造器
静态 = 类级别 = 一次;实例 = 对象级别 = 每次。这是所有顺序问题的根基。静态成员属于类本身,类加载时初始化一次,之后无论 new 多少对象都不再执行;实例成员属于每个对象,每次 new 都要重新走一遍。上面例子里第二次 new Child() 只输出 3~6 而没有 1、2,正是因为静态部分在第一次就执行完了、类已加载。
二、单个类内部:静态 → 实例 → 构造器
先不考虑继承,看一个类自己的顺序:
class Demo {
static int a = init("1 静态变量 a");
static { System.out.println("2 静态代码块"); }
int b = init("4 实例变量 b");
{ System.out.println("5 实例代码块"); }
Demo() { System.out.println("6 构造器"); }
static int init(String s) { System.out.println(s); return 0; }
}
// new Demo() 输出:1 → 2 → 4 → 5 → 6
顺序是:① 静态变量和静态代码块(按书写顺序交替执行,仅一次)→ ② 实例变量和实例代码块(按书写顺序)→ ③ 构造器。注意静态变量和静态代码块是按代码里出现的先后顺序执行的,不是「所有变量先于所有代码块」;实例变量和实例代码块同理。理解「变量和代码块按书写顺序穿插」,才能应对面试里「故意打乱顺序」的题。
三、为什么静态最先且只一次
静态成员的初始化发生在类初始化阶段(<clinit> 方法),由 JVM 保证:
类第一次被"主动使用"时触发初始化:
- new 该类对象
- 访问该类的静态变量/方法
- 反射 Class.forName
- 初始化子类会先初始化父类
JVM 把所有静态变量赋值 + 静态代码块,按顺序收集进 <clinit>() 方法
<clinit> 由 JVM 加锁保证:只执行一次、且线程安全
这解释了两件事:① 为什么静态只执行一次——<clinit> 每个类只调一次;② 为什么静态是线程安全的——JVM 用锁保证,这也是「静态内部类实现单例」和「枚举单例」线程安全的底层原因。静态代码块常用于「类级别的一次性初始化」,如加载驱动、初始化静态配置、建立静态缓存。
四、父子类:为什么「父静态 → 子静态 → 父实例 → 子实例」
加入继承后,规则是「静态全部先行(父先子后),再实例(父先子后)」。为什么?
静态部分(类加载):
初始化子类前,JVM 必须先初始化父类(子类依赖父类)
→ 父类静态 → 子类静态
实例部分(对象创建):
子类构造器第一行隐含 super() —— 必须先把父类部分构造好(地基)
→ 进入子类构造器 → 先调 super() → 父类实例块+父类构造器跑完
→ super() 返回 → 子类实例块 → 子类构造器剩余代码
核心逻辑是**「依赖决定顺序」**:子类的静态初始化依赖父类已初始化(所以父静态先),子类对象的构造依赖父类对象部分已构造(所以父类构造先)。盖房子必须先打地基(父类)再盖楼(子类),这个物理直觉正好对应初始化顺序。
五、实例代码块的真相:它被编译进构造器
一个常被误解的点:实例代码块看起来「独立于构造器」,但实际上编译器会把实例变量初始化和实例代码块的内容,复制到每个构造器的开头(super() 之后):
class Demo {
int b = 1;
{ System.out.println("实例块"); }
Demo() { System.out.println("构造器"); }
Demo(int x) { System.out.println("带参构造器"); }
}
// 编译后,两个构造器都变成:
// Demo() { super(); b=1; print("实例块"); print("构造器"); }
// Demo(int x){ super(); b=1; print("实例块"); print("带参构造器"); }
所以「实例变量/实例代码块在构造器之前执行」的本质是:它们被编译器插到了构造器体的最前面(super() 之后)。这也解释了为什么无论调哪个构造器,实例代码块都会执行——因为它被复制进了每一个构造器。实例代码块的用途是「多个构造器共享的初始化逻辑」,写一次就被插进所有构造器,避免重复。
六、综合演练与常见输出题
面试常给一段父子类代码问输出,用「静态一次·父先子后·变量块按序」逐步推:
class A {
static { System.out.print("A静态 "); }
{ System.out.print("A块 "); }
A() { System.out.print("A构造 "); }
}
class B extends A {
static { System.out.print("B静态 "); }
{ System.out.print("B块 "); }
B() { System.out.print("B构造 "); }
}
// 第一次 new B():
// A静态 B静态 A块 A构造 B块 B构造
// 第二次 new B():
// A块 A构造 B块 B构造 (静态不再执行)
推导步骤:① 类加载——初始化 B 要先初始化 A,故「A静态 → B静态」(仅第一次);② 创建对象——new B() 进 B 构造器先隐含 super(),故先跑 A 的实例块+构造「A块 A构造」,再跑 B 的「B块 B构造」。把这套「先类加载后创建、都是父先子后」的流程走一遍,任何输出题都能秒答。
记忆钩子:「静态早于实例、父类早于子类、变量与代码块按书写顺序;静态只一次(类加载 <clinit>,线程安全)、实例每次 new 都走;实例块被编译进构造器开头」。
七、常见误区与追问
- 误区:构造器是对象初始化最先执行的。 不是,静态部分(类加载时)最先且只一次,然后实例变量/实例代码块,最后才是构造器体。
- 误区:静态代码块每次 new 都执行。 只在类加载时执行一次(
<clinit>),之后无论 new 多少对象都不再执行。 - 误区:所有静态变量先于所有静态代码块执行。 是按「书写先后顺序」交替执行,实例变量和实例代码块也一样;不是「变量全部先于代码块」。
- 误区:实例代码块独立于构造器。 编译器会把它插入每个构造器的开头(super() 之后),所以调任意构造器它都执行。
- 追问:为什么父类静态先于子类静态? 初始化子类前 JVM 必须先初始化父类(子类依赖父类),所以父类的
<clinit>先于子类执行。 - 追问:静态初始化为什么是线程安全的? 静态初始化收集进
<clinit>方法,JVM 用锁保证它只执行一次且线程安全——这也是静态内部类和枚举实现单例线程安全的底层原理。 - 追问:子类构造器没写 super() 会怎样? 编译器自动在第一行插入无参 super();若父类没有无参构造器,则必须显式调用 super(参数),否则编译报错。
八、加强记忆
类初始化顺序的总纲是「静态早于实例、父类早于子类、变量与代码块按书写顺序」。要先分清两个阶段:静态部分(静态变量+静态代码块)在类加载时由 <clinit> 执行,每个类一生只一次且 JVM 保证线程安全(这是静态内部类/枚举单例线程安全的根源);实例部分(实例变量+实例代码块+构造器)在每次 new 时执行。有继承时按「依赖决定顺序」:初始化子类要先初始化父类(父静态→子静态),构造子类对象要先构造父类地基(子构造器隐含 super(),父实例块+父构造→子实例块+子构造)。一个易忽略的真相:实例代码块会被编译器插进每个构造器的开头(super() 之后),所以它「先于构造器体、且调任意构造器都执行」。完整链条:父静态→子静态(仅一次)→ 父实例块→父构造 → 子实例块→子构造(每次 new)。一句话「静态一次父先子后、实例每次父先子后、块和变量按写序、实例块编进构造器」。