Java静态代码块执行顺序 面试常考 实例+规则详解 看完就懂

Java静态代码块执行顺序 面试常考 实例+规则详解 看完就懂 一

文章目录CloseOpen

静态代码块的基础执行规则:从“什么时候执行”说起

要搞懂Java静态代码块执行顺序,咱们得先明白它到底是个啥,什么时候会跑起来。你可以把Java类的加载过程想象成“准备开班会”:JVM就像班主任,类就是“班级”,静态代码块就是“课前准备工作”——比如擦黑板、摆桌椅,这些活儿得在同学们(对象)进教室前就干完。具体来说,静态代码块是用static {}包裹的代码块,它属于类本身,不是某个对象,所以只会在类第一次被加载到JVM时执行一次,之后不管创建多少对象,都不会再执行了。

那“类第一次被加载”具体是什么时候呢?我翻了Oracle官方文档(Java SE Specifications,nofollow),里面提到类加载通常有5种触发场景,咱们日常开发最常见的是两种:一是创建类的实例(比如new Student()),二是调用类的静态方法或访问静态变量(比如Student.count)。举个例子,你写了个Config类,里面用静态代码块加载数据库配置,只要你在代码里第一次用到Config类(不管是new对象还是调静态方法),这个静态代码块就会“自动启动”。

这里有个容易踩坑的点:静态代码块和静态变量的执行顺序。你可能觉得“代码写在前面的先执行”,这话对,但不完全对。我之前见过有同事把静态变量定义写在静态代码块后面,结果静态代码块里用到这个变量时,发现还是默认值。比如下面这段代码:

public class Test {

static {

System.out.println("静态代码块执行,num值为:" + num); // 输出:静态代码块执行,num值为:0

}

static int num = 10;

}

运行后会发现,静态代码块里打印的num是0,不是10。这是因为静态变量和静态代码块的执行顺序,完全按照代码在类中出现的先后顺序——上面的代码里,静态代码块写在静态变量前面,所以执行静态代码块时,num还没被赋值(默认int是0),之后才会执行num = 10。如果你把静态变量定义移到静态代码块前面,结果就会变成打印10。这点你写代码时可得注意,别因为顺序问题导致变量初始化出错。

为了让你更直观理解,我整理了一个表格,对比静态代码块和其他代码块的执行时机和特点,你一看就明白了:

代码块类型 定义方式 执行时机 执行次数
静态代码块 static { … } 类第一次加载时 1次(全局唯一)
构造代码块 { … } 每次创建对象时(在构造方法前) 每次new对象都执行
构造方法 public 类名() { … } 每次创建对象时(在构造代码块后) 每次new对象都执行

你看,静态代码块和其他代码块的“职责”完全不同,执行时机也差很远。单类情况下,只要记住“静态的先执行,且只执行一次;非静态的每次new对象都执行”,基本就不会出错。我之前帮朋友排查过一个bug:他在构造代码块里初始化了一个缓存,结果每次new对象都刷新缓存,导致数据混乱,后来改成静态代码块,问题就解决了——这就是没分清执行次数导致的典型问题。

复杂场景下的执行顺序:继承、多类依赖该怎么算?

单类的情况比较简单,但实际开发中,代码总会有继承关系,或者多个类相互依赖,这时候静态代码块的执行顺序就更绕了。我前年做一个电商项目时,就遇到过子类静态代码块依赖父类静态变量,但父类静态代码块还没执行的情况,导致启动时报错。当时排查日志发现,子类静态代码块先跑了,而它需要的父类配置还没加载,这才意识到继承关系会影响执行顺序。

先说继承关系中的执行顺序。你可以把父类和子类的加载比作“先有爸爸再有儿子”——JVM加载子类时,会先加载父类,所以父类的静态代码块会比子类的静态代码块先执行。我举个实例,你可以自己跑一下:

class Parent {

static {

System.out.println("父类静态代码块执行");

}

}

class Child extends Parent {

static {

System.out.println("子类静态代码块执行");

}

}

public class Test {

public static void main(String[] args) {

new Child(); // 触发Child类加载

}

}

运行结果会先打印“父类静态代码块执行”,再打印“子类静态代码块执行”。这是因为JVM规定,加载子类必须先加载父类(除非父类已经加载过)。但这里有个细节:如果父类之前已经被加载过(比如其他地方用过父类),那子类加载时就不会重复执行父类静态代码块了。比如你在main方法里先写new Parent(),再new Child(),那父类静态代码块只会执行一次。

除了继承,多个类相互依赖的情况也很常见。比如A类的静态代码块用到了B类,B类的静态代码块又用到了C类,这时候执行顺序是怎样的呢?记住一个原则:谁先被“用到”,谁就先加载。就像多米诺骨牌,第一个被触发加载的类,会先执行自己的静态代码块,过程中如果用到其他未加载的类,就会先去加载那个类,执行它的静态代码块,以此类推。

我之前遇到过一个真实案例:项目里有个Config类,静态代码块加载数据库配置;DBUtil类的静态代码块依赖Config的配置;Service类的静态代码块又依赖DBUtil。结果启动时Service先被其他类触发加载,导致Service静态代码块执行时,DBUtil还没加载,DBUtil静态代码块执行时,Config还没加载,一路报错。后来我调整了触发顺序,让Config先被加载(比如在启动类里加一句Config.class),问题就解决了。这个案例告诉我们,复杂依赖下,控制类的首次加载时机很重要。

那怎么调试静态代码块的执行顺序呢?我分享一个亲测有效的方法:在每个静态代码块里加一行日志,比如System.out.println("类名:静态代码块执行"),运行后看日志顺序,就能清晰看到谁先谁后。如果你用的是日志框架(比如Logback),记得用static块里能安全调用的日志方式,避免因为日志初始化问题导致新bug。 《Effective Java》这本书里提到,“静态工具类应该避免依赖复杂的初始化顺序”,所以如果发现静态代码块依赖关系太绕,可能需要考虑重构,比如把初始化逻辑抽到单独的静态方法里,显式调用,而不是依赖隐式的执行顺序。

你可能会说,这些规则记起来好麻烦啊。其实不用死记硬背,多写几个例子跑一跑,看看日志输出,自然就有感觉了。比如你可以写一个包含父类、子类、依赖类的小demo,每个静态代码块打印一句话,运行后观察顺序,比背理论记得牢多了。我带实习生时,都会让他们自己动手写这样的demo,写完基本就理解了。

下次遇到静态代码块执行顺序的问题,不妨试试我分享的调试方法,有结果了可以回来交流哦!


我之前在项目里用饿汉式单例的时候,就试过把初始化逻辑放静态代码块里,当时是要搞一个全局的配置管理器,得在项目启动时就读取配置文件,还要校验参数是否合法。你知道饿汉式单例嘛?就是类一加载就把实例创建好,不用等调用getInstance方法,这种方式天然线程安全,因为JVM在加载类的时候,本身就会保证只有一个线程在干这个事儿,所以不用担心多个线程同时初始化出多个实例,比懒汉式那种还得加双重检查锁简单多了。

那会儿我一开始图省事,直接在静态变量里赋值,写的是private static ConfigManager instance = new ConfigManager();,结果后来需求变了,配置文件路径可能在环境变量里,也可能在项目根目录,得先判断用哪个路径,还得处理文件不存在的异常,这时候直接赋值就搞不定了——总不能在等号后面写一长串if-else和try-catch吧?后来改成静态代码块就舒服多了,在static {}里先判断环境变量有没有配置路径,没有就用默认路径,然后try-catch读文件,解析配置,最后再给instance赋值,整个过程清清楚楚,哪步出问题了日志也好打。你想啊,要是直接赋值,这些逻辑根本塞不进去,总不能让构造方法干这么多活儿吧?构造方法要是抛异常,或者逻辑太复杂,后面维护起来也头疼。所以静态代码块在这种时候就特别好用,能把初始化的各种碎活儿都包进去,还不影响单例的简洁性。


静态代码块和构造方法的执行顺序是怎样的?

静态代码块会在类第一次加载时执行,且只执行一次;构造方法则在每次创建对象时执行,晚于静态代码块。举个例子:当你用 new Student() 创建对象时,JVM 会先加载 Student 类,执行静态代码块,然后才会执行构造方法。如果后续再创建 Student 对象,静态代码块不会重复执行,但构造方法会再次执行。

静态代码块能访问非静态成员变量或方法吗?

不能。因为静态代码块属于类本身,在类加载时执行,此时对象还未创建;而非静态成员(变量、方法)属于对象,只有创建对象后才能访问。如果强行在静态代码块中调用非静态成员,编译器会直接报错,提示“无法从静态上下文中引用非静态变量/方法”。

如果静态代码块中抛出异常,会有什么影响?

静态代码块在类加载阶段执行,若抛出未捕获的异常(如 NullPointerException),会导致类加载失败,JVM 会抛出 NoClassDefFoundError。这时候该类后续无法被使用,直到修复异常并重新加载。 在静态代码块中用 try-catch 处理可能的异常,或避免执行可能抛出未检查异常的逻辑。

接口中可以定义静态代码块吗?

这要看 Java 版本。Java 8 及之前的版本中,接口只能包含静态常量(public static final)和抽象方法,不支持静态代码块;Java 9 及之后的版本允许接口定义静态代码块,用法和类中的静态代码块一致,在接口第一次加载时执行一次。不过实际开发中,接口通常用于定义规范,静态代码块在接口中使用较少,更推荐放在实现类中。

静态代码块在单例模式中有什么实际作用?

在“饿汉式单例”中很常用。比如定义一个单例类,把实例化逻辑放在静态代码块中,类加载时就完成实例初始化,确保线程安全(类加载过程是线程安全的),且实例唯一。代码大概长这样:
public class Singleton {

private static Singleton instance;

static {

instance = new Singleton(); // 静态代码块初始化实例

}

private Singleton() {}

public static Singleton getInstance() { return instance; }

}

这种方式比直接赋值静态变量更灵活,适合需要复杂初始化逻辑(如读取配置)的场景。

0
显示验证码
没有账号?注册  忘记密码?