属于类,而不属于对象
到目前为止,你写的大多数字段和方法都属于对象:每次调用 new,每个实例都会得到一份自己的字段,方法也针对那个具体对象的状态运行。static 关键字把这一点反了过来。static 成员属于类本身:它只有一份,由所有实例共享,而且无论你是否创建过对象,它都存在。
这一个区别就能解释静态字段、静态方法、常量,甚至解释为什么 main 总是 static 的。
共享的 static 字段
非静态字段让每个对象都有自己的存储位置。static 字段则让类拥有一个所有人共享的位置。经典的例子是一个记录创建了多少个对象的计数器:
注意读取计数器时写的是 User.count,通过的是类名,因为这个值不属于任何一个 User。而每个 name 都存在于各自的对象中。通过任何一个实例修改 count,所有实例都能看到新值,因为它只有一份。
static 方法
static 方法同样属于类,所以不需要对象,直接通过类来调用:
标准库背后正是这种模式:Math.max、Integer.parseInt、Arrays.sort 和 List.of 都是 static 的,它们是不需要对象就能工作的工具方法。当一项工作只依赖于参数、不依赖于任何对象自身的状态时,就用 static 方法。
最大的陷阱:static 看不到实例成员
static 方法运行时没有任何对象,所以也没有 this。这意味着它不能直接访问实例字段或调用实例方法,因为没有具体的对象可以从中读取。这是初学者使用 static 时最常犯的错误:
class Account {
int balance = 100; // instance field
static int show() {
return balance; // COMPILE ERROR: non-static field 'balance'
} // cannot be referenced from a static context
}
解决方法有两个:要么把它改成实例方法(去掉 static,这样它就有 this 了),要么显式地把对象传进去:
反过来则没有问题:实例方法可以随意读取 static 字段、调用 static 方法,因为类级别的共享数据始终存在。
用 static final 定义常量
把 static 和 final 组合起来,就得到一个常量:一个永远不能改变的共享值。按照惯例,常量用 UPPER_SNAKE_CASE 命名:
static final 是表达“属于这个类型的固定值”的惯用写法,比如标准库中的 Integer.MAX_VALUE 或 Math.PI。加上 static,就不会为每个对象浪费一份副本;加上 final,就没人能给它重新赋值。
static 初始化块
简单的 static 字段可以在声明时直接初始化。当初始化需要真正的逻辑时,比如用 HashMap 构建查找表、读取配置,就使用 static 代码块。它只在类第一次加载时运行一次,早于任何对象的创建:
无论你使用这个类多少次,这个代码块都只执行一次,所以它正适合放置一次性的、整个类范围的初始化工作。
什么时候用 static
一个简单的经验法则:
- 只有对所有实例来说确实共享的数据,才用静态字段,比如计数器、缓存、常量。如果两个对象可能合理地存放不同的值,它就应该是实例字段。
- 当结果只取决于参数、不取决于任何对象的状态时(纯粹的工具函数),用静态方法。
- 默认使用实例成员。滥用
static会悄悄把你的程序变成一堆全局状态,既难以测试,也难以理解。static是例外,而不是起点。
下一页:枚举
一个 static final 常量适合表示一个固定值,但当你有一小组固定的相关值时,比如方向、一周中的每一天、订单状态,Java 有一种专门的类型,比零散的常量更安全、表达力也更强。那就是 enum,也是下一页的内容。
常见问题
Java 中的 static 是什么意思?
static 表示字段或方法属于类本身,而不属于任何单个对象。一个 static 字段只有一份,由所有实例共享;static 方法直接通过类调用(Math.max(...)),不需要对象。相反,非静态(实例)成员在每个对象中都有一份自己的副本。
Java 中静态变量和实例变量有什么区别?
实例变量每个对象各有一个值,两个对象可以存放不同的值。static 变量只有一个值,由这个类的所有对象共享,所以通过一个对象(或通过类名)修改它,其他所有对象都能看到。每个对象自己的状态用实例字段,对整个类来说确实是全局的数据(比如计数器或常量)才用 static 字段。
Java 中 main 方法为什么是 static 的?
JVM 需要在你的类还没有任何对象时就调用 main。因为 static 方法属于类而不属于实例,运行时可以直接调用 Main.main(args),而不必先构造一个 Main。这就是为什么它的签名总是 public static void main(String[] args)。