静态代码块和静态变量按源码声明顺序执行,跨类静态依赖易因加载时机导致空指针;应优先使用holder模式延迟初始化,避免在静态上下文中直接获取spring bean。

静态代码块执行顺序不是“谁先写谁先跑”,而是严格按源码中出现的先后位置执行——哪怕逻辑上A该等B初始化完再动,JVM也只认书写顺序。跨类引用空指针,本质是“想用的还没造出来”,不是代码写错了,是时机没对上。
静态变量和静态块的执行顺序必须严格对齐
Java类加载时,静态成员按声明顺序依次处理:先默认初始化(null/0/false),再按代码顺序执行显式赋值和静态块。如果一个静态变量的初始化依赖另一个静态变量,而后者在它之后才声明或赋值,就会拿到null。
- 错误写法:先调用方法初始化变量,再定义被依赖的静态字段
- 正确做法:把被依赖的静态字段声明放在前面,确保它在调用前已完成显式赋值
- 示例:若static String config = load();里用了static Properties props,那props必须声明在config之前,且最好直接初始化:static Properties props = new Properties();
跨类静态依赖必须避免“类A等着类B先加载”这种假设
两个类之间没有强制加载顺序保障。比如ClassA里有static Service service = SpringContext.getBean(Service.class);,而ClassB又在静态块里调用ClassA.getService()——这极易失败,因为Spring容器可能还没启动,或ClassA加载时Service Bean尚未注册。
- 不要在静态上下文中直接获取Spring Bean
- 改用@PostConstruct注解的方法,在Bean完全初始化后触发赋值
- 或改用单例Holder模式:将静态变量封装进私有静态内部类,首次调用时才初始化,由JVM保证线程安全和时机可控
用延迟初始化替代类加载期硬依赖
很多空指针其实不需要在类加载时就准备好所有东西。真正需要的是“第一次用的时候有,且只初始化一次”。局部静态变量(C++)或Holder模式(Java)正是为此设计。
- Java推荐写法:private static class Holder { static final Config INSTANCE = new Config(); },然后通过Holder.INSTANCE访问
- 这样既保持了单例语义,又把初始化推迟到首次访问,绕开了类加载顺序博弈
- 比直接在类顶声明static Config config = new Config();更安全,尤其当Config构造依赖外部资源时
调试时重点看类加载日志和异常堆栈起点
空指针报在B类,根源往往在A类的静态初始化阶段。关键不是看哪行崩了,而是看ExceptionInInitializerError包裹的原始异常——它会指出具体哪个静态块或哪行赋值出了问题。
- 加JVM参数-XX:+TraceClassLoading,观察类实际加载顺序
- 在可疑静态块开头加System.out.println("Loading " + MyClass.class.getSimpleName());,确认执行流
- 遇到NPE,立刻检查堆栈最上面的clinit(即
<clinit></clinit>)方法,那里就是罪魁祸首










