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

- 唯一一份:无论你创建了多少个该类的对象,
static变量在内存中只有一份拷贝。 - 共享数据:所有对象共享这一个
static变量,一个对象修改了它的值,其他对象访问时也会看到修改后的值。 - 访问方式:可以通过
类名.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变量分配内存空间,并赋予其数据类型的默认零值(int是0,boolean是false,引用类型是null)。 - 在类加载的初始化阶段,JVM 会执行类的
<clinit>(class initializer) 方法,这个方法由编译器自动收集所有静态变量的赋值动作和静态代码块中的语句合并产生。<clinit>方法只执行一次,并且保证在多线程环境中是线程安全的。
static 变量在类首次被使用时,由 JVM 自动初始化一次。
static 变量的初始化方式
static 变量主要有三种初始化方式,它们的执行顺序是固定的。
声明时直接赋值 (Field Initialization)
这是最简单、最直接的方式。

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 变量的初始化和静态代码块的执行顺序取决于它们在源代码中出现的顺序,从上到下依次执行。

示例:
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
分析:
- JVM 加载
InitializationOrder类。 - 发现
x的声明和初始化,立即执行printAndReturn("x 初始化", 10)。 - 向下执行,遇到第一个静态代码块,立即执行其内容。
- 继续向下,发现
y的声明和初始化,立即执行printAndReturn("y 初始化", 20)。 - 继续向下,遇到第二个静态代码块,立即执行其内容。
- 至此,类的所有
static成员都已初始化完毕。 - 执行
main方法。
父类与子类的 static 初始化顺序
当一个类继承另一个类时,static 变量的初始化顺序会遵循以下规则:
- 父类的静态变量和静态代码块(按在父类中声明的顺序)。
- 子类的静态变量和静态代码块(按在子类中声明的顺序)。
示例:
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 方法开始
父类构造方法
子类构造方法
分析:
main方法首次使用Child类,触发Child类的加载。- 加载
Child类之前,必须先加载其父类Parent。 - 加载 Parent 类:按顺序初始化
Parent类的static成员。- 输出:
父类静态变量 p1 - 输出:
父类静态代码块
- 输出:
- 加载 Child 类:按顺序初始化
Child类的static成员。- 输出:
子类静态变量 c1 - 输出:
子类静态代码块
- 输出:
static初始化完成,main方法继续执行。new Child()创建对象:- 先调用父类
Parent的构造方法。 - 再调用子类
Child的构造方法。
- 先调用父类
总结与最佳实践
| 特性 | 描述 |
|---|---|
| 所属 | 属于类,不属于对象。 |
| 内存 | 在类的方法区(或元空间)中,只有一份拷贝。 |
| 初始化时机 | 在类被首次加载到 JVM 时,由 <clinit> 方法执行。 |
| 初始化顺序 | 在一个类内部,按源代码中出现的顺序从上到下执行。 |
| 继承顺序 | 先初始化父类的 static 成员,再初始化子类的 static 成员。 |
| 访问 | 推荐使用 类名.static变量名 访问。 |
最佳实践:
- 用于共享状态:当需要一个变量被所有实例共享时,例如配置信息、计数器、缓存等。
- 保持简单:
static变量的初始化逻辑应尽量简单,如果初始化非常复杂,考虑使用单例模式或依赖注入框架(如 Spring),这些模式可以更好地控制复杂对象的创建和生命周期。 - 避免可变状态:
static变量是全局共享的,修改它可能会在程序的不同地方产生意想不到的副作用,使得程序难以调试和维护,尽量将static变量声明为final(常量),如果它不需要被修改。 - 注意线程安全:虽然
<clinit>方法的执行是线程安全的,但如果一个static变量在初始化完成后,仍然被多个线程并发修改,那么就需要你自己处理同步问题,以避免数据竞争。
