杰瑞科技汇

Java static变量初始化时机和顺序是怎样的?

什么是 static 变量?

static 变量(也称为类变量)属于,而不是属于类的任何一个实例(对象),这意味着:

Java static变量初始化时机和顺序是怎样的?-图1
(图片来源网络,侵删)
  1. 唯一一份:无论你创建了多少个该类的对象,static 变量在内存中只有一份拷贝。
  2. 共享数据:所有对象共享这一个 static 变量,一个对象修改了它的值,其他对象访问时也会看到修改后的值。
  3. 访问方式:可以通过 类名.static变量名 的方式直接访问,也可以通过 对象名.static变量名 的方式访问,但推荐使用前者,因为它更清晰地表明了这是类级别的成员。
public class Counter {
    // 这是一个 static 变量,属于 Counter 类
    public static int count = 0;
    public Counter() {
        count++; // 每创建一个对象,count 就加 1
    }
}
public class Main {
    public static void main(String[] args) {
        // 通过类名访问 static 变量
        System.out.println("Initial count: " + Counter.count); // 输出: Initial count: 0
        Counter c1 = new Counter();
        System.out.println("After c1: " + Counter.count); // 输出: After c1: 1
        Counter c2 = new Counter();
        System.out.println("After c2: " + Counter.count); // 输出: After c2: 2
        // 也可以通过对象名访问,但不推荐
        System.out.println("Via c2: " + c2.count); // 输出: Via c2: 2
    }
}

static 变量的初始化时机

static 变量的初始化发生在类加载的过程中,而不是在创建对象时。

  • 当 JVM 第一次需要使用这个类时(通过 new 创建对象、访问 static 成员、或者通过反射等),JVM 的类加载器会找到这个类的 .class 文件,并将其加载到内存中。
  • 在类加载的准备阶段,JVM 会为 static 变量分配内存空间,并赋予其数据类型的默认零值int0booleanfalse,引用类型是 null)。
  • 在类加载的初始化阶段,JVM 会执行类的 <clinit> (class initializer) 方法,这个方法由编译器自动收集所有静态变量的赋值动作静态代码块中的语句合并产生。<clinit> 方法只执行一次,并且保证在多线程环境中是线程安全的。

static 变量在类首次被使用时,由 JVM 自动初始化一次。


static 变量的初始化方式

static 变量主要有三种初始化方式,它们的执行顺序是固定的。

声明时直接赋值 (Field Initialization)

这是最简单、最直接的方式。

Java static变量初始化时机和顺序是怎样的?-图2
(图片来源网络,侵删)
public class StaticInitialization {
    // 方式一:在声明时直接初始化
    public static int a = 10;
    public static String b = "Hello, Static!";
}

静态代码块 (Static Initialization Block)

当一个 static 变量的初始化逻辑比较复杂,需要多行代码才能完成时,可以使用静态代码块。

public class StaticInitialization {
    public static int c;
    // 静态代码块
    static {
        System.out.println("进入静态代码块");
        c = 100;
        // 可以执行复杂逻辑
        for (int i = 0; i < 5; i++) {
            c += i;
        }
        System.out.println("静态代码块执行完毕, c = " + c); // 输出 c = 110
    }
}

静态工厂方法或静态方法

这是一种更灵活的初始化方式,允许在运行时决定初始值。

public class StaticInitialization {
    public static int d;
    // 静态工厂方法
    public static int getD() {
        return 200;
    }
    // 静态方法
    public static void initializeD() {
        d = 300;
    }
}
// 在其他地方调用
public class Main {
    public static void main(String[] args) {
        // 静态变量 d 此时仍然是默认值 0
        System.out.println(StaticInitialization.d); // 输出: 0
        // 通过静态方法初始化
        StaticInitialization.initializeD();
        System.out.println(StaticInitialization.d); // 输出: 300
        // 通过静态工厂方法获取
        int dValue = StaticInitialization.getD();
        System.out.println(dValue); // 输出: 200
    }
}

初始化顺序详解

static 变量的初始化涉及多种方式时,它们的执行顺序是严格遵循以下规则的:

规则: 在一个类中,static 变量的初始化和静态代码块的执行顺序取决于它们在源代码中出现的顺序,从上到下依次执行。

Java static变量初始化时机和顺序是怎样的?-图3
(图片来源网络,侵删)

示例:

public class InitializationOrder {
    // 1. 声明时直接赋值
    public static int x = printAndReturn("x 初始化", 10);
    // 2. 静态代码块 1
    static {
        System.out.println("静态代码块 1 执行");
    }
    // 3. 声明时直接赋值
    public static int y = printAndReturn("y 初始化", 20);
    // 4. 静态代码块 2
    static {
        System.out.println("静态代码块 2 执行");
    }
    // 这是一个辅助方法,用于观察执行顺序
    public static int printAndReturn(String message, int value) {
        System.out.println(message + ", 值为: " + value);
        return value;
    }
    public static void main(String[] args) {
        System.out.println("main 方法开始执行");
        System.out.println("x = " + InitializationOrder.x);
        System.out.println("y = " + InitializationOrder.y);
    }
}

输出结果:

x 初始化, 值为: 10
静态代码块 1 执行
y 初始化, 值为: 20
静态代码块 2 执行
main 方法开始执行
x = 10
y = 20

分析:

  1. JVM 加载 InitializationOrder 类。
  2. 发现 x 的声明和初始化,立即执行 printAndReturn("x 初始化", 10)
  3. 向下执行,遇到第一个静态代码块,立即执行其内容。
  4. 继续向下,发现 y 的声明和初始化,立即执行 printAndReturn("y 初始化", 20)
  5. 继续向下,遇到第二个静态代码块,立即执行其内容。
  6. 至此,类的所有 static 成员都已初始化完毕。
  7. 执行 main 方法。

父类与子类的 static 初始化顺序

当一个类继承另一个类时,static 变量的初始化顺序会遵循以下规则:

  1. 父类的静态变量和静态代码块(按在父类中声明的顺序)。
  2. 子类的静态变量和静态代码块(按在子类中声明的顺序)。

示例:

class Parent {
    public static int p1 = print("父类静态变量 p1");
    static {
        print("父类静态代码块");
    }
    public Parent() {
        print("父类构造方法");
    }
    public static int print(String message) {
        System.out.println(message);
        return 0;
    }
}
class Child extends Parent {
    public static int c1 = print("子类静态变量 c1");
    static {
        print("子类静态代码块");
    }
    public Child() {
        print("子类构造方法");
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println("main 方法开始");
        new Child();
    }
}

输出结果:

父类静态变量 p1
父类静态代码块
子类静态变量 c1
子类静态代码块
main 方法开始
父类构造方法
子类构造方法

分析:

  1. main 方法首次使用 Child 类,触发 Child 类的加载。
  2. 加载 Child 类之前,必须先加载其父类 Parent
  3. 加载 Parent 类:按顺序初始化 Parent 类的 static 成员。
    • 输出: 父类静态变量 p1
    • 输出: 父类静态代码块
  4. 加载 Child 类:按顺序初始化 Child 类的 static 成员。
    • 输出: 子类静态变量 c1
    • 输出: 子类静态代码块
  5. static 初始化完成,main 方法继续执行。
  6. new Child() 创建对象:
    • 先调用父类 Parent 的构造方法。
    • 再调用子类 Child 的构造方法。

总结与最佳实践

特性 描述
所属 属于类,不属于对象。
内存 在类的方法区(或元空间)中,只有一份拷贝。
初始化时机 在类被首次加载到 JVM 时,由 <clinit> 方法执行。
初始化顺序 在一个类内部,按源代码中出现的顺序从上到下执行。
继承顺序 先初始化父类static 成员,再初始化子类static 成员。
访问 推荐使用 类名.static变量名 访问。

最佳实践:

  1. 用于共享状态:当需要一个变量被所有实例共享时,例如配置信息、计数器、缓存等。
  2. 保持简单static 变量的初始化逻辑应尽量简单,如果初始化非常复杂,考虑使用单例模式依赖注入框架(如 Spring),这些模式可以更好地控制复杂对象的创建和生命周期。
  3. 避免可变状态static 变量是全局共享的,修改它可能会在程序的不同地方产生意想不到的副作用,使得程序难以调试和维护,尽量将 static 变量声明为 final(常量),如果它不需要被修改。
  4. 注意线程安全:虽然 <clinit> 方法的执行是线程安全的,但如果一个 static 变量在初始化完成后,仍然被多个线程并发修改,那么就需要你自己处理同步问题,以避免数据竞争。
分享:
扫描分享到社交APP
上一篇
下一篇