本节摘要:封装是把字段藏进 private、经公开方法按规矩访问的做法,四种访问修饰符(private、默认、protected、public)对应本类、同包、子类、其他包四档查看权限。本节先复现"字段裸奔"被改坏的事故,再给出上锁与校验的标准写法,最后背下四档权限矩阵——protected 那一格正是第 2 章继承要用到的跨包通道。
不谈定义,先看事故。某记账系统把余额设成 public,图省事直接赋值:
class Wallet { public double balance = 100.0; // 字段裸奔:任何人都能直接改 } public class Accident { public static void main(String[] args) { Wallet w = new Wallet(); w.balance = -99999; // 没人拦:余额凭空变成负数 System.out.println(w.balance); // 输出:-99999.0 —— 脏数据已入库 } }
事故解读:问题不在这一行赋值,而在"谁都能改、改了不留痕、改错没人拦"。一旦系统变大,balance 的写入点散落在几十个类里,出现负数时你没有任何线索——没有校验、没有日志、没有唯一入口。封装解决的就是这件事:把字段锁进 private,外界只能走公开的入口方法,入口方法里做校验、记日志、维护不变量。所谓不变量,就是"任何时刻都必须成立的条件",比如余额非负——它不能靠几十个写入点自觉,只能靠唯一入口强制。
class Wallet2 { private double balance = 100.0; // 上锁:只有本类能碰 public double getBalance() { // 查看入口:只读 return balance; } public boolean deposit(double amount) { // 存款入口:校验后放行 if (amount <= 0) { System.out.println("拒绝:金额必须为正数"); return false; } balance += amount; return true; } public boolean withdraw(double amount) { // 取款入口:校验余额充足 if (amount <= 0 || amount > balance) { System.out.println("拒绝:金额非法或余额不足"); return false; } balance -= amount; return true; } } public class Locked { public static void main(String[] args) { Wallet2 w = new Wallet2(); // w.balance = -99999; // 编译报错:balance 是私有的,外界不可见 w.deposit(50); // 合法存款 w.withdraw(999999); // 输出:拒绝:金额非法或余额不足 System.out.println(w.getBalance()); // 输出:150.0 } }
注意被注释掉的那行——它现在连编译都过不了。事故从"运行时才发现脏数据"提前到"编译期就锁死",这就是封装最实际的收益。getter 与 setter 不是模板化的仪式,而是"唯一入口"带来的三个权力:校验(拒绝非法值)、监听(字段变化时通知别处)、演化(内部实现换了存法,入口签名不变,调用方不用改)。最后一条在第 4 章 String、第 5 章集合里会反复兑现。
private 只是四档权限中最严的一档。完整矩阵如下——默认(什么都不写)这一档最容易被忽视,它等于"同包公开":
| 修饰符 | 本类内部 | 同一个包 | 不同包的子类 | 不同包的其他类 |
|---|---|---|---|---|
| private | 可见 | 不可见 | 不可见 | 不可见 |
| 默认(不写) | 可见 | 可见 | 不可见 | 不可见 |
| protected | 可见 | 可见 | 可见 | 不可见 |
| public | 可见 | 可见 | 可见 | 可见 |

这张图里四档权限像四类存放处:private 是住户自己的保险柜,只有本类内部能开;默认是同层楼道储物柜,同包邻居都能用;protected 是家族祠堂——同包能进,异包子孙也能进(凭族谱,第 2 章);public 是大厅公告栏,全世界可看。
矩阵里有两格最值得记。默认档的"同包可见":初学者常以为不写修饰符就是"内部",其实它对整个包敞开——所以"工具类只给同包用"恰恰靠它实现,这比 public 克制、比 private 开放。protected 的"跨包子类可见":这是它区别于默认档的唯一一格,专为继承设计。设计类的时候,我的习惯是字段一律 private,需要给子类的用 protected 配上说明,方法从最严的档位开始放权——权限放出去容易、收回来是破坏性变更(第 2 章 final 一节会看到封口的做法)。
权限矩阵里的"同包"三个字,指向一个前置概念:包(package)。包是户籍科里的档案柜分区,作用有三:分门别类(业务类、工具类各归各柜)、命名隔离(两个柜里都能有同名类,全限定名不冲突)、权限边界(默认与 protected 两档权限都按"柜"划分)。同一个包不只是"目录结构相同",还要求包声明语句一致;import 的作用只是"开柜时免写全名",不改变任何权限。
// 一个把默认权限用在刀刃上的例子:包内工具类 class PacketCounter { // 默认权限:同包可见,包外不可见 static int count = 0; static void inc() { count++; } } public class Registry { public static void main(String[] args) { PacketCounter.inc(); // 同包内随便用 PacketCounter.inc(); System.out.println(PacketCounter.count); // 输出:2 } }
完整案例走一遍:背景:体检系统里 Person 类的年龄字段被三个模块直接赋值,某次来了个 -1 岁和 320 岁的记录。操作:age 改 private,提供 setAge 入口做 0 到 150 的区间校验,越界抛出参数异常(异常机制第 7 章展开,这里先用它表达"拒绝登记")。结果:脏数据在入口处被拦下,并留下明确的拒绝原因;合法调用方只需把"直接赋值"改成"调方法",一行换一行。解读:封装的代价是调用方多写一对括号,换回的是不变量永不被打破。变式:若需求变成"年龄只许读不许改",只留 getter 不写 setter;若字段要在构造后永不变,直接上 final(下一节)配合构造器注入——锁的档位按需求升级。
字段上了锁,还剩两种特殊登记方式没有讲:挂在门牌上归全楼共用的 static,和一次登记终身生效的 final。下一节把它们一起收进第 1 章的最后一格。