本节摘要:static 把成员的归属从"每个对象一份"改成"全类一份"——静态字段是全楼共用的门牌,静态方法属于类不属于对象,因此不能使用 this 与实例成员。final 是铁证:修饰变量则一旦赋值不可更改,修饰方法则禁止重写,修饰类则封族谱。本节用实例计数器与常量类两个案例讲透归属问题,并解释"静态方法里为什么看不到实例字段"。
先做一道判断题:一栋楼的总户数,应该登记在哪一户的档案上?登记在哪一户都不对——它是整栋楼的属性,任何一户落户或迁出都会改它,但它不属于任何人。Java 对应的机制就是 static:被 static 修饰的成员,归属从对象上收到类上,全类只有一份。
class Building { static int residentCount = 0; // 门牌计数:全楼一份,挂在类上 String name; // 普通字段:每户一份 Building(String name) { this.name = name; residentCount++; // 每次落户(构造器执行),全楼计数加一 } } public class StaticDemo { public static void main(String[] args) { Building a = new Building("张三"); Building b = new Building("李四"); Building c = new Building("王五"); // 三种读法完全等价,都指向同一份门牌 System.out.println(Building.residentCount); // 输出:3(推荐:类名点静态成员) System.out.println(a.residentCount); // 输出:3 System.out.println(b.residentCount); // 输出:3 } }
三行输出全是 3,而且改任何一处全变——这恰恰证明 residentCount 在内存里只有一份。推荐用类名访问静态成员,读代码的人一眼知道"这是门牌,不是某户的资产";用对象访问静态成员在语法上合法,语义上有误导性,部分团队规范直接禁用。
静态成员随着类加载而初始化,先于任何对象存在。这解释了一个次序事实:静态方法的调用不需要任何对象——工具类如 Math、Objects(第 4 章 4.3 节)整类全是静态方法,一次实例化都不发生。也解释了它的边界:静态方法执行时"当前对象"不存在,所以 this 无从指向,实例字段无从解析。1.1 节那个"无法从静态上下文引用非静态变量"的编译报错,根源就在这里——不是语法洁癖,是"没有我本人,何谈我的资产"。
main 方法为什么是 static 的,到这里也清楚了:程序入口被调用时还没有任何对象,JVM 必须能不依赖实例直接调起它。
门牌的便利背后是暗面:静态字段是全局共享状态。单线程下它只是"改一处全变";多线程下两个窗口同时读改同一块门牌,就会互相覆盖——这正是第 8 章同步机制的第一个案例场景。这里先把结论立住:静态可变字段是并发事故的高发区,能不用就不用;要用,要么做成不可变(配合 final),要么明确加同步。
与之相对的静态常量则是安全且推荐的用法,也是最常见的"static 加 final"组合拳:
class Config { // 常量:全类一份、终身不变。命名惯例全大写、下划线分词 public static final double TAX_RATE = 0.13; public static final String REGISTRY_NAME = "类图户籍科"; } public class ConstDemo { public static void main(String[] args) { System.out.println(Config.REGISTRY_NAME + " 税率:" + Config.TAX_RATE); // 输出:类图户籍科 税率:0.13 // Config.TAX_RATE = 0.15; // 编译报错:常量不可重新赋值 } }
final 的语义是"终态",落点随修饰对象而变,三种用法容易混,分别看代码:
class Family { static final int FLOOR_COUNT = 18; // 用法一:锁变量 —— 引用或值终身不变 final String surname; // 实例常量:必须在构造器里完成赋值 Family(String surname) { this.surname = surname; // 唯一的赋值机会 } final void familyRule() { // 用法二:锁方法 —— 子类不得重写 System.out.println(surname + "家的家规:所有成员遵守统一规则"); } } final class SealedLedger { } // 用法三:锁类 —— 封族谱,不许有子类 public class FinalDemo { public static void main(String[] args) { final Family f = new Family("赵"); // f = new Family("钱"); // 编译报错:f 这个引用不能再指向别处 f.familyRule(); // 输出:赵家的家规:所有成员遵守统一规则 } }
有一处精细的区分必须单独说:final 修饰引用类型变量,锁的是"引用"不是"对象内容"。f 不能再指向另一份档案,但 f 所指对象内部的非 final 字段照样能改。想要"内容也不可变",得靠类自身的设计(String 就是这么做的,第 4 章)或用不可变集合(第 5 章)。方法参数上的 final 同理:锁的是参数名在这个方法内的重新赋值,防止维护者无意间把入参改掉。
锁方法与锁类主要服务于继承场景:final 方法防止子类改写关键逻辑(第 2 章方法重写的"封口"),final 类直接断绝族谱——JDK 里 String 就是 final 类,任何人不得写它的子类,这是字符串不可变体系的围墙。具体机制等学完第 2 章的继承再看这一段会更有体感。
背景:物业要给每户一个唯一编号,编号规则是"楼栋前缀加三位序号",且序号必须连续不重复、总数可查。操作:序号用静态字段累加(全楼一份),编号用实例 final 字段(落户即定、终身不改):
class Household { static int seq = 0; // 全楼共用的发号器 static final String PREFIX = "A栋-"; final String id; // 每户的编号:定一次,终身不改 String holder; Household(String holder) { seq++; // 发号器加一(多线程下需同步,第 8 章处理) this.id = PREFIX + String.format("%03d", seq); this.holder = holder; } } public class IdDemo { public static void main(String[] args) { Household h1 = new Household("张三"); Household h2 = new Household("李四"); System.out.println(h1.id + " / " + h2.id); // 输出:A栋-001 / A栋-002 System.out.println("已登记户数:" + Household.seq); // 输出:已登记户数:2 // h1.id = "A栋-999"; // 编译报错:id 是铁证,不可重写 } }
结果:编号连续、全楼计数准确,且任何代码都无法事后篡改编号。解读:static 负责共享的发号器,final 负责个体的不可篡改,两者分工而非替代。变式:若编号需要按年重置,把发号器从静态字段升级为按年维护的静态 Map(第 5 章);若要多窗口并发发号,把 seq 换成原子类或加同步(第 8 章 8.2 节正是这个案例的并发版)。
第 1 章至此收束:登记表、入籍手续、权限、门牌与铁证都齐了。下一章进族谱科——把一份份档案用 extends 连成家族,看祖产怎么共享、章程怎么改写、同一张叫号单怎么让不同住户各自应答。