第10章 封装:让对象保护自己的规则
第10章 封装:让对象保护自己的规则
上一章我们用Student类把学号、姓名、分数和是否及格放到了一起。但还有一个严重问题:字段是公开的,外部可以随便改。
Studentstudent=newStudent("S001","Tom",90);student.score=999;student.name="";这显然不合理。分数不应该超过 100,姓名不应该是空字符串。
封装要解决的问题是:对象内部的数据不能被外部随便破坏,修改数据必须经过对象自己定义的规则。
一、封装不是为了多写 getter/setter
很多人学封装时只记住:
private字段publicgetterpublicsetter然后机械生成:
privateintscore;publicintgetScore(){returnscore;}publicvoidsetScore(intscore){this.score=score;}如果 setter 不做任何校验,外部仍然可以:
student.setScore(999);这只是换了一种方式破坏对象。
真正的封装,是让对象保护自己的规则。
二、private:把字段藏起来
publicclassStudent{privateStringid;privateStringname;privateintscore;}private表示只能在类内部访问。外部不能:
student.score=90;这样外部无法绕过对象规则直接修改字段。
三、通过构造方法保证初始状态合法
publicclassStudent{privateStringid;privateStringname;privateintscore;publicStudent(Stringid,Stringname,intscore){if(id==null||id.isEmpty()){thrownewIllegalArgumentException("学号不能为空");}if(name==null||name.isEmpty()){thrownewIllegalArgumentException("姓名不能为空");}if(score<0||score>100){thrownewIllegalArgumentException("分数必须在0到100之间");}this.id=id;this.name=name;this.score=score;}}这样对象一创建就是合法的。不要创建一个半坏的对象,再希望后面每个地方都小心判断。
四、getter:允许外部读取必要信息
字段 private 后,外部如果需要读取,提供 getter:
publicStringgetName(){returnname;}publicintgetScore(){returnscore;}读取:
System.out.println(student.getName());getter 的意义是暴露必要信息。不是所有字段都必须有 getter。只暴露外部确实需要知道的内容。
五、不要无脑 setter,用业务方法
分数可以更新,但要校验:
publicvoidupdateScore(intscore){if(score<0||score>100){thrownewIllegalArgumentException("分数必须在0到100之间");}this.score=score;}方法名用updateScore,比setScore更有业务含义。
外部调用:
student.updateScore(95);如果传 999,会抛异常,规则不会被破坏。
六、final:表达创建后不应该改变的字段
学号通常创建后不应改变:
privatefinalStringid;final 字段必须在构造方法里赋值,赋值后不能再改。
publicStudent(Stringid,Stringname,intscore){this.id=id;this.name=name;this.score=score;}如果你再写:
this.id="S002";编译器会阻止。
final 的意义是把“不应该变”的规则交给编译器检查。
七、完整 Student 封装版
publicclassStudent{privatefinalStringid;privateStringname;privateintscore;publicStudent(Stringid,Stringname,intscore){if(id==null||id.isEmpty()){thrownewIllegalArgumentException("学号不能为空");}if(name==null||name.isEmpty()){thrownewIllegalArgumentException("姓名不能为空");}this.id=id;this.name=name;updateScore(score);}publicStringgetId(){returnid;}publicStringgetName(){returnname;}publicintgetScore(){returnscore;}publicvoidrename(StringnewName){if(newName==null||newName.isEmpty()){thrownewIllegalArgumentException("姓名不能为空");}this.name=newName;}publicvoidupdateScore(intscore){if(score<0||score>100){thrownewIllegalArgumentException("分数必须在0到100之间");}this.score=score;}publicbooleanisPassed(){returnscore>=60;}}这里的设计:
id不允许修改,所以 final。name可以改,但必须通过rename。score可以改,但必须通过updateScore。isPassed是根据 score 计算出来的行为。
八、封装和异常
现在代码里出现了:
thrownewIllegalArgumentException("分数必须在0到100之间");异常后面会系统讲。这里先理解它的含义:调用方传了非法参数,对象拒绝接受,并抛出错误。
为什么不只是打印?
System.out.println("分数不合法");如果只是打印,程序可能继续使用一个错误对象。构造方法里发现非法数据时,更合理的是阻止对象创建。
九、封装实战:银行账户
账户不能随便改余额。余额只能通过存款和取款变化。
publicclassBankAccount{privatefinalStringaccountId;privateintbalanceCent;publicBankAccount(StringaccountId,intinitialBalanceCent){if(accountId==null||accountId.isEmpty()){thrownewIllegalArgumentException("账户不能为空");}if(initialBalanceCent<0){thrownewIllegalArgumentException("初始余额不能为负数");}this.accountId=accountId;this.balanceCent=initialBalanceCent;}publicStringgetAccountId(){returnaccountId;}publicintgetBalanceCent(){returnbalanceCent;}publicvoiddeposit(intamountCent){if(amountCent<=0){thrownewIllegalArgumentException("存款金额必须大于0");}balanceCent+=amountCent;}publicvoidwithdraw(intamountCent){if(amountCent<=0){thrownewIllegalArgumentException("取款金额必须大于0");}if(amountCent>balanceCent){thrownewIllegalArgumentException("余额不足");}balanceCent-=amountCent;}}这个类没有setBalanceCent。因为外部不应该直接设置余额。
这就是封装的真正价值:通过对象方法控制状态变化。
十、封装设计的几个问题
1. 字段是否应该 private?
绝大多数字段应该 private。除非是非常特殊的常量。
2. 是否需要 getter?
外部确实需要读,就提供。否则不要暴露。
3. 是否需要 setter?
先问:这个字段是否允许任意修改?如果不是,就不要机械 setter。
4. 是否应该 final?
创建后不应该变的字段,优先考虑 final。
5. 校验放哪里?
和字段规则强相关的校验,应该放在对象内部。比如分数范围、账户余额不能为负。
十一、常见错误
1. 字段全 public
对象规则完全失控。
2. getter/setter 全生成
看似封装,实际外部仍然随便改。
3. 构造方法不校验
非法对象能被创建,后面所有地方都要补救。
4. 用打印代替错误处理
对象内部发现非法状态,只打印不阻止,风险很大。
5. 方法名没有业务含义
setStatus不如pay、cancel、ship清楚。
十二、本章练习
- 把上一章的 Book 类改成封装版:
- id final
- title 不为空
- priceCent 不能小于 0
- 提供
changePrice方法
实现
BankAccount,测试存款、取款、余额不足。定义
Product:
- 商品 id 不可变。
- 商品名不能为空。
- 库存不能为负。
- 提供
increaseStock和decreaseStock。
- 思考:为什么
setStock(int stock)不如increaseStock/decreaseStock清楚?
十三、本章总结
封装不是形式,而是规则保护。
你需要掌握:
- private 隐藏字段。
- 构造方法保证对象初始状态合法。
- getter 只暴露必要信息。
- 不要机械生成 setter。
- 用业务方法表达状态变化。
- final 表示创建后不应变化。
- 对象内部应该维护自己的规则。
- 非法参数可以用异常阻止。
下一章进入继承和多态。继承能复用代码,但也容易被滥用。我们会先讲它解决什么问题,再讲它的边界。
