本节摘要:类是对象的模板,对象是类的实例——类定义字段(资产登记项)与方法(技能业务),对象按模板在堆内存里持有各自独立的字段副本,方法全类共享一份。本节拆开 class 关键字背后的登记规则:字段和方法各登记在谁名下、两个对象为什么互不干扰、成员变量与局部变量的本质差异。它是全书的地基,后面七窗口都要回到这张登记表。
户籍科墙上挂着一张空白登记表:姓名、房产面积、会办的业务。这张挂在墙上的表就是类——它规定了"凡在我这里登记的住户,必须申报哪些资产、会办哪些业务",但它自己不是住户,档案柜里没有它的位置。当一个真实的人按这张表填好、盖上编号、放进档案柜,才产生对象。Java 里的对应关系一行就能说清:类是编译期的类型定义,对象是运行时 new 出来的、堆内存里的实体。
先写一张最小的登记表,然后让它两次落户:
// 登记表模板:住户类 class Resident { // 资产登记项(实例字段):每个住户各填一份 String name; double area; // 技能业务(实例方法):操作的是"我本人"的资产 void report() { System.out.println(name + " 的房产面积:" + area + " 平米"); } } public class Demo { public static void main(String[] args) { Resident r1 = new Resident(); // 第一次落户:档案柜里出现第一份档案 Resident r2 = new Resident(); // 第二次落户:第二份档案,与第一份无关 r1.name = "张三"; r1.area = 89.5; r2.name = "李四"; r2.area = 120.0; r1.report(); // 输出:张三 的房产面积:89.5 平米 r2.report(); // 输出:李四 的房产面积:120.0 平米 r2.area = 130.0; // 只改了李四的资产 System.out.println(r1.area); // 输出:89.5 —— 张三的档案纹丝不动 } }
这段代码验证了登记表的第一条铁律:字段跟着对象走,一人一份。改 r2 的面积,r1 毫不知情。变量 r1、r2 本身只是"档案编号牌"(引用),真正的档案在堆内存里;把 r1 赋值给 r3,只是又做了一块指向同一份档案的编号牌,Resident r3 = r1; r3.area = 1; 之后 r1 的面积也变成 1——因为动的是同一份档案。

模板上登记两类条目,归属完全不同。字段是资产登记项:写在类里、但在对象落地时按份分配——两个对象各有一套 name 与 area,互不牵连。方法是技能业务:写一份、全类共用——report 方法的方法体在内存里只有一份,r1.report 与 r2.report 跑的是同一段代码,差别只在于执行时 this 指向谁(1.2 节展开 this)。
这个"字段按份、方法按份共享"的结构,解释了一个很多初学者没意识到的现象:方法里可以直接写 name,因为方法执行时总是"附着"在某个具体对象上,name 就解析为那个对象的字段。但 main 方法不行——它是 static 的,不属于任何对象,没有"我本人"可指,所以在静态方法里直接写 name 会编译报错"无法从静态上下文中引用非静态变量"。这个报错的机制根源,1.4 节讲 static 时会彻底说透。
字段与方法之外,登记表上还有一类特殊栏目:局部变量。它和成员变量(字段)名字相近,身份完全不同:
| 对比项 | 成员变量(字段) | 局部变量 |
|---|---|---|
| 登记位置 | 类体内、方法外 | 方法内或参数列表 |
| 归属 | 跟随对象或类 | 跟随方法调用栈帧 |
| 默认值 | 有(数值零、布尔假、引用空) | 无,用前必须赋初值 |
| 生命周期 | 与对象一致 | 方法返回即消失 |
默认值这条差异值得单独记一笔:字段不赋初值也能用(int 默认 0、引用类型默认 null),局部变量不赋初值编译直接报错。这不是语言设计随意,而是初始化责任不同——对象的字段在构造阶段由 JVM 统一清零(1.2 节的入籍仪式),而局部变量的清零会白白浪费一次赋值。
空讲结构容易飘,来一个从头到尾的完整过程。背景:物业系统要记录每户的房产变更,要求每次改动都留下"谁在什么时候改的"。操作:在登记表上加一个变更次数字段和一个变更方法,方法内部先核对身份再修改资产:
class ResidentV2 { String name; double area; int changeCount = 0; // 变更次数:初始为零的字段 boolean changeArea(double newArea, String operator) { if (operator == null || operator.isEmpty()) { System.out.println("变更被拒绝:必须登记经办人"); return false; // 手续不全,不予变更 } if (newArea < 0) { System.out.println("变更被拒绝:面积不能为负"); return false; } this.area = newArea; // this.area 指本对象的资产,1.2 节细讲 this.changeCount++; System.out.println(name + " 的面积已变更为 " + area + ",累计变更 " + changeCount + " 次"); return true; } } public class Case1 { public static void main(String[] args) { ResidentV2 r = new ResidentV2(); r.name = "王五"; r.changeArea(90.0, "前台小赵"); // 输出:王五 的面积已变更为 90.0,累计变更 1 次 r.changeArea(-5, "前台小赵"); // 输出:变更被拒绝:面积不能为负 r.changeArea(95.0, null); // 输出:变更被拒绝:必须登记经办人 System.out.println(r.changeCount); // 输出:1 —— 被拒的变更没有计入 } }
结果:三次调用只有一次成功,changeCount 只记了一次。解读:changeCount 之所以"记得住",是因为它是字段——它的生命周期跟随对象,方法结束后并不消失;假如把它写进方法里当局部变量,每次调用都从零开始,记录就废了。变式:把需求改成"全物业今年总变更次数",字段就要搬到 static 名下(全楼一块门牌,1.4 节);改成"变更历史明细",就需要一个数组字段按次追加——登记表的结构跟着需求走,这正是面向对象设计的日常。
下一节办入籍手续:new 的那一瞬间发生了什么、构造器怎么保证"档案不空着就上架"、this 在窗口里怎么喊"我本人"。