导读:本期聚焦于小伙伴创作的《Java里类的初始化顺序究竟如何决定?初始化阶段与执行顺序说明》,敬请观看详情。父类和子类都有静态变量、静态块、实例变量和构造器时,JVM加载一个类会先递归加载父类,随后按代码书写顺序执行父类静态初始化,再处理子类静态部分。静态内容只在类首次被主动使用时执行一次,实例相关的初始化要等新建对象才触发,且父类构造器先于子类实例块运行。理解ClassLoader的链接与初始化边界,能避免空指针和配置未加载的问题。

在Java程序运行中,类的初始化顺序由JVM规范严格定义,它直接决定了静态资源何时就绪、实例字段何时赋值。很多诡异的空指针其实都源于对初始化阶段的误解。下面通过具体代码来拆解整个流程。

Java里类的初始化顺序究竟如何决定?初始化阶段与执行顺序说明

一、类的生命周期与初始化触发条件

Java类从被加载到虚拟机内存开始,会经历加载、链接(验证、准备、解析)、初始化、使用和卸载几个阶段。其中初始化阶段才是真正执行类构造器<clinit>()方法的过程,这个方法由编译器自动收集类中的所有静态变量赋值动作和静态语句块合并产生。

并不是类一被加载就立刻初始化。JVM规定只有在遇到new、getstatic、putstatic、invokestatic这四条字节码指令,或者反射调用,或者子类初始化触发父类初始化等“主动使用”场景时,才会执行初始化。如果只是声明一个类的数组类型,或者引用了类里编译期常量,都不会触发初始化。

二、静态部分的初始化顺序

在初始化阶段,JVM会先保证父类完全初始化,再初始化子类。同一个类内部,静态变量和静态代码块严格按照在源文件中出现的先后顺序执行。下面这段代码可以清晰看到顺序:

public class Parent {
    public static String pStaticVar = initPVar();
    static {
        System.out.println("父类静态块执行");
    }
    private static String initPVar() {
        System.out.println("父类静态变量赋值");
        return "parent";
    }
}

public class Child extends Parent {
    public static String cStaticVar = initCVar();
    static {
        System.out.println("子类静态块执行");
    }
    private static String initCVar() {
        System.out.println("子类静态变量赋值");
        return "child";
    }
}

当首次主动使用Child类时,控制台输出依次为:父类静态变量赋值、父类静态块执行、子类静态变量赋值、子类静态块执行。这说明父类静态内容整体优先于子类,且各自内部顺序与书写一致。

这种顺序带来的实际影响是,如果子类静态块依赖父类静态变量,由于父类先完成,所以不会有问题;但若父类静态块依赖子类才会设置的某个值,则必然拿到null。因此在架构设计上,应禁止父类静态逻辑反向依赖子类。

三、实例对象的初始化顺序

当使用new创建对象时,在类已初始化的前提下,会调用<init>()实例构造器。顺序为:父类实例变量赋值和父类实例块按序执行,然后父类构造器,接着子类实例变量和实例块按序执行,最后子类构造器本体。

public class Base {
    private String bVar = "base_var";
    {
        System.out.println("父类实例块:" + bVar);
    }
    public Base() {
        System.out.println("父类构造器");
    }
}

public class Derived extends Base {
    private String dVar = "derived_var";
    {
        System.out.println("子类实例块:" + dVar);
    }
    public Derived() {
        System.out.println("子类构造器");
    }
}

新建Derived对象后输出为:父类实例块:base_var、父类构造器、子类实例块:derived_var、子类构造器。可见实例字段的赋值早于构造器方法体,但晚于父类全部实例初始化。

这一机制常导致在父类构造器中调用被子类重写的方法时,子类字段还未赋值,从而读到默认值。因此编码规范要求:构造器中避免调用可重写方法,正是为了规避这种半初始化状态。

四、综合顺序与常见误区

把静态和实例放在一起,完整顺序是:父类静态变量与静态块、子类静态变量与静态块、父类实例变量与实例块、父类构造器、子类实例变量与实例块、子类构造器。下面用一张简表归纳:

阶段执行内容次数
类加载初始化父类静态->子类静态仅一次
对象实例化父类实例->父类构造->子类实例->子类构造每次new

一个典型误区是认为静态块一定先于main方法执行。实际上main方法本身就在某个类的静态上下文中,该类在main被调用前已初始化,所以写在main前的静态块会先跑,但其他无关类的静态块不会自动执行。

另一个坑是接口中的静态变量。接口本身不要求子接口或实现类初始化时一并初始化父接口,只有在真正用到父接口静态字段时才触发。这和类继承的强制父类初始化不同,需要特别留意。

五、利用顺序保障配置安全加载

在真实项目里,常把数据库连接池参数放在静态块中读取配置文件。由于静态块只跑一次且早于任何实例创建,可以保证后续所有对象拿到的都是已就绪的连接池。示例:

public class DataSourceHolder {
    private static final Properties cfg = new Properties();
    static {
        try {
            cfg.load(DataSourceHolder.class.getResourceAsStream("/db.properties"));
            System.out.println("配置加载完成:" + cfg.getProperty("url"));
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
    public static String getUrl() {
        return cfg.getProperty("url");
    }
}

如果配置加载失败,静态块抛出错误会使类初始化失败,JVM后续对该类的所有使用都会收到ExceptionInInitializerError,这比在对象里才发现连接地址为null要安全得多。

理解并善用这套顺序,不仅能写出可预测的启动逻辑,也能在排查“为什么值为空”“为什么块没执行”时迅速定位到是主动使用时机不对,还是继承链上的顺序预期错误。

Java类初始化执行顺序修改时间:2026-08-02 08:24:15

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。