Java static 关键字从浅入深

前言

static 是 Java 里非常常见的关键字。它的核心意思是:这个成员属于类,而不是属于某一个对象。

如果把类比作一张“设计图”,对象就是按设计图造出来的一台台机器。普通字段是每台机器自己的零件,static 字段则像挂在工厂墙上的公共看板,所有机器看到的是同一份。


一、static 的基本概念

1.1 static 修饰什么

static 可以修饰变量、方法、代码块、内部类。

常见形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo {
static int count;

static void hello() {
System.out.println("hello");
}

static {
System.out.println("class init");
}

static class Helper {
}
}

不能直接修饰普通外部类,也不能修饰局部变量。

1.2 static 的一句话理解

普通成员依赖对象,static 成员依赖类。

所以普通成员通常用 对象.成员 访问,静态成员通常用 类名.成员 访问。


二、static 变量

2.1 类变量与实例变量

static 修饰的变量叫类变量。它在类加载后只有一份,被这个类的所有对象共享。

实例变量属于对象,每 new 一次就有一份。

1
2
3
4
5
6
7
8
9
class User {
static int total;
String name;

User(String name) {
this.name = name;
total++;
}
}

这里 name 是每个用户自己的名字,total 是所有用户共同维护的总人数。

2.2 使用场景

static 变量适合存放“全班共享”的数据。

常见场景:

1
2
3
4
5
6
7
8
9
10
public class AppConfig {
public static final String APP_NAME = "demo";
}
public class Counter {
private static int total;

public Counter() {
total++;
}
}

适合放常量、计数器、共享配置、缓存引用。

不适合放用户登录状态、请求参数、订单数据等会因对象或线程变化而变化的数据。


三、static 方法

3.1 静态方法的特点

静态方法属于类,可以不创建对象直接调用。

1
2
Math.max(1, 2);
Integer.parseInt("123");

静态方法中不能直接访问非静态字段,因为它执行时不一定有对象。

1
2
3
4
5
6
7
class Demo {
String name;

static void test() {
// System.out.println(name); // 编译错误
}
}

原因很简单:你站在工厂办公室里,能看公共看板,但不能知道某一台机器里私有零件的状态,除非你拿到那台机器的引用。

3.2 使用场景

静态方法适合无状态工具方法。

1
2
3
4
5
public class StringUtils {
public static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}

如果方法强依赖对象状态,就不要写成 static


四、static 代码块

4.1 static 代码块什么时候执行

静态代码块在类初始化时执行,并且一个类只执行一次。

1
2
3
4
5
class Demo {
static {
System.out.println("load Demo");
}
}

常见触发时机包括:创建对象、访问静态字段、调用静态方法、反射加载类。

4.2 初始化顺序

大致顺序是:

1
类加载 -> 静态字段赋值和静态代码块 -> 实例字段和实例代码块 -> 构造方法

静态内容先于对象存在。它像工厂开门前先装好的公共设备,对象只是后面生产出来的产品。


五、static 内部类

5.1 静态内部类

静态内部类不依赖外部类对象。

1
2
3
4
5
6
class Outer {
static class Helper {
}
}

Outer.Helper helper = new Outer.Helper();

普通内部类依赖外部类对象。

1
2
3
4
5
6
7
class Outer {
class Inner {
}
}

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

5.2 使用场景

静态内部类常用于把辅助类收在外部类内部,表达“它只服务于这个外部类”的关系。

经典用法是 Holder 单例:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() {
}

private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return Holder.INSTANCE;
}
}

它利用类初始化的线程安全特性,实现懒加载和安全发布

外部类加载

  • 当 JVM 加载 Singleton 类时:
    • 会加载外部类的类信息(类结构、方法、字段信息)。
    • 不会立刻加载静态内部类 Holder
  • 也就是说,即使外部类被加载,内部类仍然“睡着”,不会初始化其中的 static 字段。

类比:外部类是房子,内部类是保险箱。房子建好了,保险箱没开,里面的东西不会动。

静态内部类的加载延迟到第一次使用

这里的第一次使用是指:

  • 调用 Holder 的 static 字段或方法

六、static 与内存模型

6.1 static 存在哪里

很多旧资料会说 static 变量在方法区。更准确地说,类的元数据在方法区的实现中,HotSpot 从 JDK 8 开始使用元空间保存类元数据;而静态字段的实际存储与类对象相关,底层实现细节和 JVM 版本有关。

学习时可以先记住:static 成员和类生命周期绑定,不和某个实例对象绑定。

6.2 类加载与初始化

Java 类从被 JVM 认识到可使用,会经历加载、验证、准备、解析、初始化等阶段。

static 来说,重点是:

准备阶段会给静态变量分配默认值。

初始化阶段会执行显式赋值和 static 代码块。

1
static int count = 10;

准备阶段:count = 0

初始化阶段:count = 10

6.3 线程安全思考

static 变量是共享的,共享就可能有并发问题。

1
2
3
4
5
private static int count;

public static void add() {
count++;
}

count++ 不是原子操作,多线程下可能丢失更新。项目中可用 AtomicInteger、锁、不可变对象,或者避免共享可变静态状态。


七、项目中如何使用 static

7.1 推荐使用

推荐用于常量:

1
public static final int MAX_RETRY = 3;

推荐用于无状态工具方法:

1
2
3
public static String normalize(String value) {
return value == null ? "" : value.trim().toLowerCase();
}

推荐用于 Holder 单例:

1
2
3
private static class Holder {
private static final Service INSTANCE = new Service();
}

7.2 谨慎使用

谨慎使用可变静态变量。

1
public static List<String> users = new ArrayList<>();

它容易带来全局状态污染、测试互相影响、线程安全问题和难以追踪的数据变化。

项目经验里,static 最大的问题不是不会用,而是用得太方便。


八、面试如何考察 static

8.1 高频问题

常见问题:

static 变量和实例变量区别是什么?

static 方法能不能访问普通成员?

static 代码块什么时候执行?

类加载初始化顺序是什么?

static final 和普通 static 有什么区别?

静态内部类为什么能实现单例?

静态变量线程安全吗?

8.2 回答思路

先讲归属:static 属于类,普通成员属于对象。

再讲生命周期:static 随类加载初始化,普通成员随对象创建初始化。

再讲访问限制:静态上下文没有 this,不能直接访问实例成员。

最后讲项目风险:共享可变状态要注意并发和测试污染。


总结

static 的本质是“属于类,而不是属于对象”。

用得好,它可以表达常量、工具方法、类级共享资源和单例结构;用得不好,它会变成全局变量,制造并发问题和测试污染。

记住一句话:能不共享就不共享,必须共享就尽量不可变;如果共享可变状态,就要认真考虑线程安全和生命周期。