首页 / Java 入门教程 / static 关键字

Java 入门教程

static 关键字

本教程共 100 篇 · 第 35 篇 · 更新于 2026-08-05 · 约 7 分钟阅读

JavaJava 入门教程static静态变量静态方法类加载

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:写常量的标准姿势

staticfinal 搭配,表示「全类共享 + 不可修改」,这是定义常量的固定写法。

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 之后搬到了元空间。

两种说法角度不同,结论一致:**静态变量不在任何实例的内存里**。所以对象被回收了它依然健在,这也解释了它为什么能跨对象共享。