1.3 户口本上锁:封装与访问控制


1.3 户口本上锁:封装与访问控制

本节摘要:封装是把字段藏进 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 可见 可见 可见 可见

图 1-3 四档权限:从保险柜到公告栏

图 1-3 四档权限:从保险柜到公告栏

这张图里四档权限像四类存放处: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(下一节)配合构造器注入——锁的档位按需求升级。

本节要点回顾

  • 封装的实质是唯一入口:字段 private 化,读写走公开方法,换来校验、监听、演化三个权力,脏数据拦截从运行期提前到编译期
  • 四档权限矩阵背到条件反射:private 本类、默认同包、protected 加跨包子类、public 全放开
  • 默认档不等于内部:它是"同包公开",常用于包内工具类;protected 的独有价值是跨包子类可见,为第 2 章继承预留的通道
  • 包是档案柜分区:分类、命名隔离、权限边界三合一,import 只是免写全名、不放大权限
  • getter 与 setter 不是仪式:没有校验需求的只读字段可以只留 getter;永不变的字段直接 final 加构造注入

字段上了锁,还剩两种特殊登记方式没有讲:挂在门牌上归全楼共用的 static,和一次登记终身生效的 final。下一节把它们一起收进第 1 章的最后一格。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U