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