7.5 静态成员与类级状态
前置知识:实例字段、实例方法、封装和构造方法
预计时间:25 分钟
完成标志:能区分类级与实例级成员,并能识别共享可变状态的生命周期和并发风险
“这一本书是否借出”属于某个对象;“这个 Book 类成功创建过多少个对象”属于整类。示例让你把后一种状态放进 static 成员。
1. static 字段不随每个实例重复
final class Book {
private static int totalCreated;
private final String title;
Book(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("blank title");
}
this.title = title;
totalCreated++;
}
String title() {
return title;
}
static int totalCreated() {
return totalCreated;
}
}new Book("Java 核心技术");
new Book("深入理解计算机系统");
System.out.println(Book.totalCreated()); // 2通过类名访问静态成员能清楚表达其归属。Java 也允许通过对象表达式访问 static 成员,但容易让人误以为每个对象各有一份,应避免。
这个计数记录成功创建次数,不是当前存活对象数;类也设为 final,避免把随后构造失败的子类误计为成功。示例只适合单线程、小范围计数教学,int 还可能溢出。totalCreated++ 是读、加、写的组合,并非原子操作;多个线程并发创建对象会丢计数。生产代码可按需求使用 AtomicInteger、LongAdder、锁或外部指标系统。
静态状态也不是“全宇宙只有一份”。它与定义类的 Class 对象相关;不同类加载器可以各自加载同名类并拥有独立静态状态。测试、应用服务器和插件系统尤其要留意这个边界。
2. 静态方法没有当前对象
class TextRules {
static boolean hasText(String value) {
return value != null && !value.isBlank();
}
}调用 TextRules.hasText(title) 不需要先创建 TextRules。静态方法中没有 this,也不能直接访问实例字段:
class Book {
String title;
static int titleLength(Book book) {
return book.title.length(); // 通过明确的对象参数访问
}
}实例方法可以访问 static 成员,因为它既有当前对象,也知道所属类;反方向不成立,除非静态方法先获得一个对象引用。
3. 常量通常是 static final
class LendingRules {
static final int MAX_LOAN_DAYS = 30;
}命名常量常用大写下划线。final 只禁止变量重新赋值,不会自动让所指对象不可变:
static final java.util.List<String> TAGS = new java.util.ArrayList<>();
// TAGS 不能改指向另一列表,但仍可 TAGS.add(...)公开常量应优先使用基本类型、String、枚举或真正不可变类型。不要把 public static final 当成共享可变容器的免责标签。
4. 工具类只放无实例状态的操作
public final class PriceMath {
private PriceMath() {
throw new AssertionError("no instances");
}
public static long addCents(long left, long right) {
return Math.addExact(left, right);
}
}final 防止继承,private 构造防止外部实例化。若方法需要大量可替换依赖、配置或共享缓存,把它们都塞进 static 工具类会让测试和生命周期管理更困难,此时普通对象更合适。
5. Java 17 的经典启动入口
Java 17 启动器寻找下面的入口签名:
public static void main(String[] args) {
// ...
}入口是 static,所以启动器无需先知道怎样构造应用类。String... args 也可作为等价声明形式。其他 Java 版本或专门运行环境可能提供额外入口机制,本文以 Java 17 命令行启动器为准。
6. 静态初始化有生命周期成本
静态字段初始化器和 static 初始化块在类初始化时执行,按源代码顺序运行。创建实例、调用静态方法、读取非编译期常量的静态字段等主动使用会触发相应类的初始化。读取编译期常量通常不触发初始化,因为值可能已写进调用方字节码;发布库后修改这类常量,还需重新编译调用方才能保证看到新值。
避免在静态初始化中进行慢网络请求、读取易失败的外部配置或启动后台线程。初始化中抛出的非 Error 异常会包装为 ExceptionInInitializerError;原本就是 Error 则直接传播。类会被标为初始化失败,之后再次主动使用通常得到 NoClassDefFoundError。
专业速查
| 成员 | 需要接收者 | 状态归属 | 典型用途 |
|---|---|---|---|
| 实例字段 | 是 | 每个对象 | 书名、借阅状态 |
| 实例方法 | 是 | 可操作当前对象 | 借书、还书 |
| static 字段 | 否 | 类与类加载器 | 常量、受控共享状态 |
| static 方法 | 否 | 无当前对象 | 纯工具、工厂、入口 |
离开这一页前
把书名和借出状态放在实例侧,把创建计数与纯工具操作放在类侧,并解释静态方法为什么没有 this。再检查 static final 引用是否仍指向可变对象、count++ 在并发下会不会丢更新,以及不同类加载器为何可能各有一份静态状态。
规范入口:JLS §12.4.2:类的初始化步骤。