【Java】封装:你的数据不该被随意触碰
【Java】封装——语言根基三封装你的数据不该被随意触碰一、引言一个“太开放”的惨痛教训二、封装是什么三、为什么需要封装3.1 保护数据完整性3.2 隐藏内部实现允许未来修改3.3 控制访问权限3.4 降低耦合四、怎么实现封装4.1 访问修饰符一张表搞定4.2 Getter/Setter 不是万能的4.3 防御性拷贝面试常考4.4 业务方法 Setter五、完整案例安全的银行账户六、进阶封装在真实项目中的样子6.1 不可变对象真正的终极封装6.2 封装与单元测试6.3 封装与常见框架6.4 Java 9 模块化封装七、常见误区误区1所有属性都要有 getter/setter误区2封装就是 private getter/setter误区3封装让代码变长没必要误区4常量也要 private八、面试高频题背下来Q1封装是什么为什么要封装Q2有 getter/setter 就算封装吗Q3反射能破坏封装吗Q4封装影响性能吗Q5什么是防御性拷贝Q6Collections.unmodifiableList 就绝对安全了吗九、思考题含深度解析题1下面代码有什么问题题2设计 Temperature 类题3进阶如何让一个类不能被继承且内部状态完全不可变十、总结封装你的数据不该被随意触碰从入门到面试一篇就够了真正完整版一、引言一个“太开放”的惨痛教训想象你开了一家银行为了“方便客户”你把金库的钥匙挂在门口上面贴了张纸条“请自行取用用完放回”。结果可想而知。这很荒唐对吧但在编程世界里很多初学者就在做同样荒唐的事BankAccountaccountnewBankAccount();account.balance-1000;// 余额变成负数account.owner;// 账户名为空没有任何保护数据可以被随意篡改成非法状态。这就是为什么我们需要封装。二、封装是什么官方定义将数据属性和操作数据的方法行为捆绑在一起并隐藏内部实现细节对外提供受控的访问接口。一句话人话把数据锁进保险柜只开一个小窗口让外部操作。┌─────────────────────┐ │ 外部调用方 │ │ 只能通过窗口操作 │ └──────────┬──────────┘ │ ┌──────▼──────┐ │ public方法 │ ← 小窗口可控 ├─────────────┤ │ private属性 │ ← 保险柜隐藏 │ private方法 │ └─────────────┘三、为什么需要封装3.1 保护数据完整性没有封装 → 数据可以被随意篡改// 没有封装StudentsnewStudent();s.age-5;// 编译通过但数据非法有了封装 → 加校验阻止非法数据// 有封装publicvoidsetAge(intage){if(age0||age150){thrownewIllegalArgumentException(年龄非法);}this.ageage;}3.2 隐藏内部实现允许未来修改// 外部只调用 getFullName()publicStringgetFullName(){returnfirstName lastName;}// 内部随便改外部无感知publicStringgetFullName(){returnlastNamefirstName;// 改成姓在前名在后}3.3 控制访问权限publicdoublegetBalance(){returnbalance;}// 人人可看publicvoiddeposit(doubleamount){...}// 人人可存privatevoidaudit(){...}// 只有内部能用3.4 降低耦合封装后每个类是独立的黑盒。调用方只需要知道“能做什么”不需要知道“怎么做的”。四、怎么实现封装4.1 访问修饰符一张表搞定修饰符同类同包子类任意private✓×××默认不写✓✓××protected✓✓✓×public✓✓✓✓最佳实践属性几乎总是private特例public static final常量可以公开方法public对外、private内部辅助、protected给子类包级私有默认用于同一包内的协作类是一种被低估的封装手段// package-private同包可访问对外隐藏classInternalHelper{voiddoPackageLevelWork(){...}}4.2 Getter/Setter 不是万能的坏习惯给每个 private 字段无脑生成 getter/setter。// 这样写等于把属性公开了封装了个寂寞publicStringgetPassword(){returnpassword;}// 暴露密码好习惯只暴露真正需要的。publicclassUser{privateStringpassword;// 不提供 getter只提供验证publicbooleancheckPassword(Stringinput){returnthis.password.equals(input);}// 修改需要原密码验证publicvoidchangePassword(StringoldPwd,StringnewPwd){if(checkPassword(oldPwd)){this.passwordnewPwd;}}}4.3 防御性拷贝面试常考当返回可变对象时不要直接返回内部引用// 危险外部可以修改内部数据publicDategetBirthday(){returnthis.birthday;}// 安全返回副本publicDategetBirthday(){returnnewDate(this.birthday.getTime());}// 集合也同理——注意unmodifiableList 只防结构修改不防元素修改publicListItemgetItems(){returnCollections.unmodifiableList(items);}// 如需深度防御元素也应为不可变对象或返回深拷贝publicListItemgetItemsDeepCopy(){returnitems.stream().map(Item::copy).collect(Collectors.toList());}4.4 业务方法 Setter// 暴露了内部结构publicvoidsetItems(ListItemitems){...}publicvoidsetTotalPrice(doubleprice){...}// 提供业务方法publicvoidaddItem(Itemitem){items.add(item);totalPriceitem.getPrice();}五、完整案例安全的银行账户publicclassBankAccount{// 私有属性privateStringaccountNo;privatedoublebalance;// 注意生产环境需考虑线程安全privateStringpassword;privateintfailCount;privatebooleanlocked;privatestaticfinalintMAX_FAIL3;// 构造方法publicBankAccount(StringaccountNo,Stringpassword,doubleinitBalance){this.accountNoaccountNo;this.passwordpassword;this.balanceinitBalance;}// 只读属性无setterpublicStringgetAccountNo(){returnaccountNo;}// 查看余额需要密码publicdoublegetBalance(Stringpassword){verify(password);returnbalance;}// 存款公开publicvoiddeposit(doubleamount){if(amount0)thrownewIllegalArgumentException(金额必须大于0);balanceamount;resetFailCount();}// 取款需要密码publicvoidwithdraw(Stringpassword,doubleamount){verify(password);if(amountbalance)thrownewIllegalArgumentException(余额不足);balance-amount;resetFailCount();}// 私有方法密码验证privatevoidverify(Stringinput){if(locked)thrownewIllegalStateException(账户已锁定);if(!this.password.equals(input)){failCount;if(failCountMAX_FAIL)lockedtrue;thrownewSecurityException(密码错误);}}privatevoidresetFailCount(){failCount0;}}生产环境进阶上述balance在多线程下不安全。实际项目中可用AtomicLong或synchronizedprivatefinalAtomicLongbalancenewAtomicLong();publicvoiddeposit(longamount){balance.addAndGet(amount);}使用示例BankAccountaccnewBankAccount(123456,1234,1000);acc.deposit(500);// 存款不需要密码acc.withdraw(1234,200);// 取款需要密码System.out.println(acc.getBalance(1234));// 1300// acc.balance -100; // 编译错误private 不可访问六、进阶封装在真实项目中的样子6.1 不可变对象真正的终极封装不可变对象天然线程安全是最彻底的封装形式publicfinalclassImmutablePerson{privatefinalStringname;privatefinalListStringtags;publicImmutablePerson(Stringname,ListStringtags){this.namename;// 防御性拷贝不信任外部传入的集合this.tagsList.copyOf(tags);// Java 9返回不可变集合}publicStringgetName(){returnname;}publicListStringgetTags(){returntags;}// 已是不可变无需再包装}6.2 封装与单元测试封装太好可能不利于测试。解决方案publicclassCalculator{privateintinternalState;// 提供 package-private 的测试钩子仅测试代码可访问intgetInternalStateForTest(){returninternalState;}}// 测试代码同包可直接调用TestvoidtestInternalState(){assertEquals(0,calculator.getInternalStateForTest());}6.3 封装与常见框架框架如何“破坏”封装是否安全Spring反射调用 private 构造器/字段框架级后门业务代码不应模仿Lombok编译时生成 getter/setter不破坏只是减少样板代码Jackson反射读取 private 字段进行序列化可接受但可用JsonIgnore控制6.4 Java 9 模块化封装// module-info.javamodulecom.example.banking{exportscom.example.banking.api;// 对外公开exportscom.example.banking.internaltocom.example.test;// 仅对测试模块公开}七、常见误区误区1所有属性都要有 getter/setter错。这等于把属性变相公开。只暴露真正需要的接口。误区2封装就是 private getter/setter错。这只是语法层面。真正的封装是把业务逻辑和数据放在一起让外部“命令对象做什么”而不是“问对象要数据”。// 坏外部计算if(acc.getBalance()amount)acc.setBalance(acc.getBalance()-amount);// 好对象自己处理acc.withdraw(amount);误区3封装让代码变长没必要错。多写的几行代码换来的是可维护性和安全性。大型项目中没有封装的代码最终会变成“意大利面条式代码”指结构混乱难以维护的代码。误区4常量也要 private错。真正的常量public static final可以公开因为它是不可变的publicstaticfinaldoublePI3.1415926;// 公开没问题八、面试高频题背下来Q1封装是什么为什么要封装答把数据和操作捆绑隐藏内部细节。目的①保护数据完整性 ②隔离变化 ③控制权限 ④降低耦合。Q2有 getter/setter 就算封装吗答不算。那是“贫血模型”。真正封装应该提供业务方法比如withdraw()而不是setBalance()。Q3反射能破坏封装吗答能。setAccessible(true)可以绕过 private。但这是框架层面的后门业务代码不应使用。封装防的是粗心不是恶意。Q4封装影响性能吗答现代 JVM 的 JIT 编译器会对 getter/setter 进行内联优化最终性能与直接访问字段几乎无差别。不要为了微小的性能牺牲设计。Q5什么是防御性拷贝答返回内部可变对象Date、List时返回副本或只读视图防止外部修改内部状态。Q6Collections.unmodifiableList就绝对安全了吗答不是。它只防止增删改结构但如果列表中的元素本身是可变的外部仍可修改元素内容。需要元素也是不可变对象或返回深拷贝。九、思考题含深度解析题1下面代码有什么问题publicclassOrder{publicListItemitemsnewArrayList();publicvoidaddItem(Itemitem){items.add(item);}}答案属性是public外部可以直接order.items.clear()破坏数据应改为private并提供getItems()返回Collections.unmodifiableList(items)注意unmodifiableList只防结构修改如果Item可变外部仍能order.getItems().get(0).setPrice(0)。如需完全保护Item也应为不可变类题2设计 Temperature 类要求存储摄氏度可获取华氏度不能低于绝对零度-273.15°C。参考答案publicclassTemperature{privatedoublecelsius;privatestaticfinaldoubleABSOLUTE_ZERO-273.15;publicTemperature(doublecelsius){setCelsius(celsius);}publicdoublegetCelsius(){returncelsius;}publicdoublegetFahrenheit(){returncelsius*9/532;}publicvoidsetCelsius(doublec){if(cABSOLUTE_ZERO)thrownewIllegalArgumentException(低于绝对零度);this.celsiusc;}publicvoidsetFahrenheit(doublef){setCelsius((f-32)*5/9);// 复用校验逻辑}}题3进阶如何让一个类不能被继承且内部状态完全不可变答案publicfinalclassImmutableConfig{privatefinalMapString,Stringsettings;publicImmutableConfig(MapString,Stringsettings){this.settingsMap.copyOf(settings);// 防御 不可变}publicMapString,StringgetSettings(){returnsettings;// 已经是不可变Map直接返回安全}}关键点final classfinal字段 构造器防御性拷贝 不提供修改方法 返回不可变视图。十、总结境界特征青铜会用privateget/set白银会在 setter 里写校验黄金提供业务方法不暴露内部数据铂金会用不可变对象、防御性拷贝、包级私有钻石理解模块化封装能在封装与测试之间找到平衡记住好的封装是让别人用你的类时想出错都难。