static 关键字
本教程共 100 篇 · 第 35 篇 · 更新于 2026-08-05 · 约 7 分钟阅读
35. static 关键字
本节目标:搞懂 static 把成员从「对象级别」提升为「类级别」,学会用静态变量、静态方法、静态代码块,并理解它们和实例成员的生命周期差异。
前面写的成员变量属于「每个对象各一份」。有些东西整个类只需要一份,比如统计创建了多少个对象、数学常数 π。这时用 static。
不用 static 会怎样
先看一个真实的需求:统计程序里一共创建了多少个用户对象。
如果计数器是普通成员变量,每个对象都自带一个,各数各的,永远是 1。
public class BadCounter {
int count = 0; // 每个对象各一份,各自从 0 开始
BadCounter() {
count++; // 只加自己那一份
}
}
```bash
new 一百次,一百个对象里躺着一百个值为 1 的 `count`。谁也不知道总数。
问题的本质是:**这个数据描述的是「整个类」的状况,不是某个对象的属性**。它不该跟着对象走。
`static` 就是干这个的——把成员挂到类身上,全类共用一份。
## 静态变量:属于类,不属于某个对象
加 `static` 的变量叫静态变量(也叫类变量),所有对象共享同一份。
```java
public class Counter {
static int count = 0; // 静态变量,全类共享
Counter() {
count++; // 每创建一个对象就加一
}
public static void main(String[] args) {
new Counter();
new Counter();
new Counter();
System.out.println(Counter.count); // 输出 3
}
}
三次 new 累加到同一个 count 上,结果是 3。不管 new 多少个 Counter,Counter.count 都指向同一块内存。
静态变量在类加载时就分配好,不跟着某个对象走。哪怕一个对象都不创建,它照样存在,照样能访问。
Note静态变量通过「类名点变量名」访问最规范,如
Counter.count。虽然用对象点也能编译通过,但读代码的人容易误会成「这是对象特有的」,团队规范里一般禁止这么写。
静态方法:没有 this
静态方法属于类,调用时不需要对象,直接用类名点方法名。它内部没有 this,因为压根不知道是「哪个对象」在调。
public class MathUtil {
static int square(int x) {
return x * x; // 只用到参数,不碰任何对象状态
}
}
```java
调用时不用 new:
```java
int r = MathUtil.square(5); // 直接类名调用,结果 25
判断一个方法该不该加 static,有个简单标准:方法体里用不用得到某个对象的状态。用不到,就是纯粹的输入到输出的映射,加 static 更合适。
真实的编译错误
初学者最常撞的墙,是在静态方法里直接用实例成员:
public class Wrong {
int count = 0; // 实例变量,没有 static
public static void main(String[] args) {
count++; // 编译错误
}
}
```java
`javac` 报出来的原文长这样:
```text
Wrong.java:5: error: non-static variable count cannot be referenced from a static context
count++;
^
1 error
翻译过来就是:静态上下文里引用不了非静态变量。
原因很直白。main 是静态方法,运行时可能一个对象都没创建,count 属于哪个对象?编译器答不上来,只能拒绝。
修的办法有两条。要么把变量也改成 static,要么先造一个对象再点:
public class Fixed {
int count = 0;
public static void main(String[] args) {
Fixed obj = new Fixed();
obj.count++; // 明确说清是 obj 这个对象的 count
System.out.println(obj.count);
}
}
```bash
调用实例方法也是同样的规矩,同样会报 `non-static method xxx() cannot be referenced from a static context`。
> [!WARNING]
> 反过来不受限制:实例方法里可以随便调静态方法、用静态变量。因为对象存在时,类肯定已经加载好了。方向是单向的——实例能看到静态,静态看不到实例。
`main` 方法本身就是 static,原因也在这里。程序刚启动时堆里空空如也,JVM 手上没有任何对象可以点,只能从静态方法进门。
## 静态代码块:类加载时跑一次
`static {}` 块在类第一次被加载时执行,且只执行一次,专门用来做一次性初始化。
```java
public class Config {
static String path;
static {
path = "config/default.properties"; // 类加载时初始化
System.out.println("配置已加载");
}
}
它比构造方法更早执行,而且不管 new 多少次对象只跑一遍。适合加载配置、注册驱动这类「全局只做一次」的活。
静态变量的显式赋值和静态代码块,按书写顺序从上往下执行,可以理解为它们被编译器合并成了同一段初始化代码。
类什么时候被加载
「类加载」听起来抽象,其实触发条件很具体。下面几种情况会让 JVM 首次加载并初始化一个类:
| 触发动作 | 举例 |
|---|---|
| 创建对象 | new Config() |
| 访问静态变量 | 读或写 Config.path |
| 调用静态方法 | MathUtil.square(5) |
| 反射操作 | Class.forName("Config") |
| 启动类(含 main) | java Config |
反过来说,如果一个类从头到尾没被碰过,它的静态代码块永远不会执行。别指望写了静态块它就会自动运行。
有个例外要留个心眼:访问 static final 修饰的编译期常量,不会触发类加载。编译器直接把值内联到调用处了,压根没去找那个类。
static final:写常量的标准姿势
static 和 final 搭配,表示「全类共享 + 不可修改」,这是定义常量的固定写法。
public class Circle {
public static final double PI = 3.141592653589793; // 常量:全类一份且不可改
public static final int MAX_RADIUS = 1000;
}
```bash
命名约定是全大写加下划线,一眼就能看出这是常量。
只写 `final` 不写 `static` 也能定义不可变的值,但那样每个对象都会存一份完全相同的数据,纯属浪费。常量加 static 是标配。
## 工具类的惯用写法
一堆静态方法凑成的类叫工具类,`Math`、`Arrays`、`Collections` 都是这个路子。
工具类不需要被实例化——你不会想 new 一个 Math 出来。所以惯例是把构造方法设成 private,从语法上堵死实例化的路:
```java
public final class StringUtil {
private StringUtil() { // 私有构造,禁止外部 new
}
public static boolean isEmpty(String s) {
return s == null || s.isEmpty();
}
public static String reverse(String s) {
return new StringBuilder(s).reverse().toString();
}
}
逐行看这个类的设计意图:
final修饰类,防止被继承——工具类没有子类的意义。private StringUtil()挡住外部new StringUtil(),编译期就报错。- 两个方法都是 static,调用时写
StringUtil.isEmpty(s),干净利落。
静态导入
Java 5 引入了静态导入,能把静态成员的类名前缀省掉。
import static java.lang.Math.PI;
import static java.lang.Math.abs;
public class Demo {
public static void main(String[] args) {
System.out.println(PI); // 不用写 Math.PI
System.out.println(abs(-3)); // 不用写 Math.abs
}
}
```bash
好处是代码短,坏处是读代码的人不知道 `abs` 从哪来的。除了 `Math` 和测试断言这类高度熟悉的场景,平时别滥用。
## 实例成员 vs 静态成员
| 对比项 | 实例成员(无 static) | 静态成员(有 static) |
|--------|----------------------|----------------------|
| 归属 | 每个对象一份 | 全类一份 |
| 访问方式 | 对象点成员 | 类名点成员 |
| 有无 this | 有 | 无 |
| 创建时机 | new 对象时 | 类加载时 |
| 销毁时机 | 对象被回收时 | 类被卸载时(通常到程序结束) |
| 能否访问对方 | 能访问静态成员 | 不能直接访问实例成员 |
> [!TIP]
> 判断加不加 static 的简单标准:这个东西是「每个对象各有一份」(如人的姓名),还是「全类共用一份」(如计数器、工具方法)。前者实例,后者静态。
## 别把 static 当银弹
static 用着方便,但滥用会带来实实在在的麻烦。
**静态变量活得太久。** 它随类加载而生,通常撑到程序结束。往静态集合里塞数据不清理,内存就一直涨。这是内存泄漏的常见来源。
**静态方法难以替换。** 静态方法不能被重写,写单元测试时想换成假实现会很别扭。业务逻辑写成静态方法,测试成本会明显上升。
**共享状态带来并发风险。** 多个线程同时改一个静态变量,不加同步就会出错。第 97 章会讲怎么处理。
一个经验法则:**无状态的纯计算用 static,有状态的业务对象别用**。
## 常见疑问
**问:静态方法能被子类重写吗?**
不能。子类里写一个签名相同的静态方法,那叫「隐藏」而不是「重写」,两者行为完全不同。重写在运行时按对象的实际类型来选方法,隐藏则在编译期按变量声明的类型来选。加 `@Override` 注解会直接编译报错,可以用它来验证。
**问:静态代码块和构造方法哪个先执行?**
静态代码块先。它在类加载阶段跑,构造方法在创建对象时才跑,而创建对象必然发生在类加载之后。而且静态块只跑一次,构造方法每次 new 都跑。第 38 章会用一段可运行代码把完整顺序打印出来。
**问:静态变量存在哪里?**
按 JVM 规范的说法,静态变量归属「方法区」里的类信息。落到 HotSpot 的具体实现,从 JDK 7 起静态字段实际存放在堆中那个 Class 对象里,类的元数据则在 JDK 8 之后搬到了元空间。
两种说法角度不同,结论一致:**静态变量不在任何实例的内存里**。所以对象被回收了它依然健在,这也解释了它为什么能跨对象共享。