
父类静态变量和静态代码块先于子类执行
Java 类加载时,静态成员按「类声明顺序」在类初始化阶段执行,且父类优先于子类。这不是编译期决定的,而是 JVM 在首次主动使用该类(比如 new、调用静态方法、访问静态字段等)时触发类初始化,此时会递归确保父类已初始化。
常见错误现象:NullPointerException 出现在子类静态字段里,只因它引用了尚未初始化完成的父类静态字段——其实不是“未初始化”,而是父类静态块还没轮到执行完。
- 父类
static变量赋值 → 父类static代码块 → 子类static变量赋值 → 子类static代码块 - 如果父类静态块里调用了子类的静态方法,会导致子类提前初始化(即“被动触发”),但此时子类的静态变量可能还未赋值(只声明、未初始化),结果是默认值(如
null、0) -
ClassLoader.loadClass("X")不会触发初始化;只有Class.forName("X")(无参)或显式访问才会
构造器调用链:从 Object 到子类,不跨过父类构造器
每次 new 实例,JVM 强制要求子类构造器第一行必须是 super() 或 this() 调用(哪怕没写,编译器自动补 super())。这意味着构造器执行顺序和继承链完全一致,且无法跳过中间某层。
容易踩的坑:在父类构造器里调用被子类重写的方法,此时子类实例字段还未初始化(仍是默认值),但方法体已执行子类版本——这是典型的“过早暴露 this”问题。
立即学习“Java免费学习笔记(深入)”;
- 执行顺序:父类实例变量赋值 → 父类构造器体 → 子类实例变量赋值 → 子类构造器体
- 父类构造器中调用
method(),而子类重写了它,JVM 仍走动态绑定,执行子类方法,但此时this.field是默认值(如null) - 避免在构造器中调用非
private/ 非final方法,尤其不要依赖子类字段状态
静态变量 vs 实例变量:初始化时机差一个「类加载阶段」
静态变量属于类,随类初始化完成;实例变量属于对象,每次 new 时才分配并初始化。两者生命周期、作用域、触发时机完全不同,混用会导致意料外的 null 或旧值残留。
典型场景:工具类里用静态 Map 缓存数据,但 Map 的 key 是某个子类实例 —— 如果这个子类还没被加载,那它的 class 对象都不存在,更别说作为 key 安全使用了。
- 静态变量在类初始化时完成(一次),实例变量在每次构造器执行中完成(多次)
- 子类继承父类静态变量,但不会重新初始化;而继承的实例变量会在每次 new 子类时重新赋值
- 若静态变量是可变对象(如
static List<string> cache = new ArrayList()</string>),多个子类实例共享同一份引用,要注意线程安全与污染风险
实战验证:用 System.out.println 打点看真实顺序
别猜,直接打印。JVM 规范写得清楚,但具体到你的代码,唯一可信的是日志输出。重点打在静态变量声明处、静态块、构造器开头——注意别打在字段初始化表达式里(如 int x = logAndReturn(1)),那会干扰判断。
示例片段:
class A {
static { System.out.println("A static"); }
{ System.out.println("A init"); }
A() { System.out.println("A ctor"); }
}
class B extends A {
static { System.out.println("B static"); }
{ System.out.println("B init"); }
B() { System.out.println("B ctor"); }
}
// 输出一定是:
// A static
// B static
// A init
// A ctor
// B init
// B ctor
如果实际输出不符,大概率是类被多次加载(不同 ClassLoader)、或有 static final 编译期常量导致提前内联——这时候就得查类加载器和字节码了。
最复杂也最容易被忽略的点:同一个类被两个不同的 ClassLoader 加载,它们的静态变量完全隔离,互不影响。这种情况下,“父子类”关系只在源码层面成立,在运行时可能是两套独立的类体系。







