
类的初始化(Initialization)发生在 JVM 第一次主动使用该类时,由 <clinit> 方法执行。这个方法由编译器自动生成,用于合并类中所有静态变量赋值语句和静态代码块(static{}),按源码书写顺序从上到下执行。父子类之间的初始化顺序受“首次主动使用”和“类加载过程依赖”双重影响,并非简单“先父后子”,而是遵循“用到才初始化、父类先于子类初始化”的原则。
触发 <clinit> 的五种主动使用场景
只有以下情况会触发类初始化(即执行 <clinit>),其他如单纯声明引用、子类引用父类静态成员(不涉及子类自身初始化)、数组定义等,均不会触发:
创建该类的实例(new) 调用该类的静态方法 访问该类的静态字段(且该字段不是常量(即非 static final 编译期常量)) 反射调用(如 Class.forName("xxx")) 初始化一个类时,若其父类尚未初始化,则先触发父类初始化
父子类静态初始化的真实顺序:用到才走、父先于子
当子类被首次主动使用时,JVM 检查其父类是否已初始化;若未初始化,则**先递归完成父类的 <clinit> 执行**,再执行子类的 <clinit>。注意:父类初始化只发生一次,即使多个子类先后触发,父类 <clinit> 也仅执行一次。
例如:
class Parent { static { System.out.println("Parent clinit"); } static int x = getValue(); static int getValue() { System.out.println("Parent getValue"); return 1; }}class Child extends Parent { static { System.out.println("Child clinit"); } static int y = getValue(); static int getValue() { System.out.println("Child getValue"); return 2; }}
执行 new Child() 时输出为:
Parent clinitParent getValueChild clinitChild getValue
说明:父类 <clinit> 完全执行完毕(含静态块 + 静态变量赋值)后,才开始子类 <clinit>。
静态字段访问对初始化的影响(关键细节)
访问 Parent.x 会触发 Parent.<clinit>;但访问 Child.x(继承自父类的静态字段)同样只触发 Parent 初始化,不触发 Child 初始化。除非该字段是 static final 且为编译期常量(如 public static final int VAL = 42;),此时连 Parent.<clinit> 都不会触发——值直接内联进调用处字节码。
反例:若 Parent.x 是 static final String s = "hello";,则 System.out.println(Parent.x) 不触发初始化;但若写成 static final String s = new String("hello");(非常量表达式),就会触发 <clinit>。
接口与类初始化的区别
接口也有 <clinit>,但规则不同:实现接口的类初始化时,不会导致其父接口初始化;只有当程序首次访问接口的静态字段(非常量)时,才触发该接口初始化。此外,接口的父接口初始化也不受子接口是否初始化影响——接口间无“强制先行初始化”约束,除非显式访问其静态成员。

评论(0)