当前位置: 首页 > news >正文

【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:什么是防御性拷贝?
      • Q6:`Collections.unmodifiableList` 就绝对安全了吗?
    • 九、思考题(含深度解析)
      • 题1:下面代码有什么问题?
      • 题2:设计 Temperature 类
      • 题3(进阶):如何让一个类不能被继承且内部状态完全不可变?
    • 十、总结

封装:你的数据不该被随意触碰

从入门到面试,一篇就够了(真正完整版)

一、引言:一个“太开放”的惨痛教训

想象你开了一家银行,为了“方便客户”,你把金库的钥匙挂在门口,上面贴了张纸条:“请自行取用,用完放回”。

结果可想而知。这很荒唐,对吧?

但在编程世界里,很多初学者就在做同样荒唐的事:

BankAccountaccount=newBankAccount();account.balance=-1000;// 余额变成负数!account.owner="";// 账户名为空!

没有任何保护,数据可以被随意篡改成非法状态。

这就是为什么我们需要封装

二、封装是什么?

官方定义:将数据(属性)和操作数据的方法(行为)捆绑在一起,并隐藏内部实现细节,对外提供受控的访问接口。

一句话人话把数据锁进保险柜,只开一个小窗口让外部操作。

┌─────────────────────┐ │ 外部调用方 │ │ 只能通过窗口操作 │ └──────────┬──────────┘ │ ┌──────▼──────┐ │ public方法 │ ← 小窗口(可控) ├─────────────┤ │ private属性 │ ← 保险柜(隐藏) │ private方法 │ └─────────────┘

三、为什么需要封装?

3.1 保护数据完整性

没有封装 → 数据可以被随意篡改:

// 没有封装Students=newStudent();s.age=-5;// 编译通过,但数据非法

有了封装 → 加校验,阻止非法数据:

// 有封装publicvoidsetAge(intage){if(age<0||age>150){thrownewIllegalArgumentException("年龄非法");}this.age=age;}

3.2 隐藏内部实现,允许未来修改

// 外部只调用 getFullName()publicStringgetFullName(){returnfirstName+" "+lastName;}// 内部随便改,外部无感知publicStringgetFullName(){returnlastName+firstName;// 改成姓在前名在后}

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.password=newPwd;}}}

4.3 防御性拷贝(面试常考)

当返回可变对象时,不要直接返回内部引用:

// 危险:外部可以修改内部数据publicDategetBirthday(){returnthis.birthday;}// 安全:返回副本publicDategetBirthday(){returnnewDate(this.birthday.getTime());}// 集合也同理——注意:unmodifiableList 只防结构修改,不防元素修改publicList<Item>getItems(){returnCollections.unmodifiableList(items);}// 如需深度防御,元素也应为不可变对象或返回深拷贝publicList<Item>getItemsDeepCopy(){returnitems.stream().map(Item::copy).collect(Collectors.toList());}

4.4 业务方法 > Setter

// 暴露了内部结构publicvoidsetItems(List<Item>items){...}publicvoidsetTotalPrice(doubleprice){...}// 提供业务方法publicvoidaddItem(Itemitem){items.add(item);totalPrice+=item.getPrice();}

五、完整案例:安全的银行账户

publicclassBankAccount{// 私有属性privateStringaccountNo;privatedoublebalance;// 注意:生产环境需考虑线程安全privateStringpassword;privateintfailCount;privatebooleanlocked;privatestaticfinalintMAX_FAIL=3;// 构造方法publicBankAccount(StringaccountNo,Stringpassword,doubleinitBalance){this.accountNo=accountNo;this.password=password;this.balance=initBalance;}// 只读属性(无setter)publicStringgetAccountNo(){returnaccountNo;}// 查看余额需要密码publicdoublegetBalance(Stringpassword){verify(password);returnbalance;}// 存款(公开)publicvoiddeposit(doubleamount){if(amount<=0)thrownewIllegalArgumentException("金额必须大于0");balance+=amount;resetFailCount();}// 取款(需要密码)publicvoidwithdraw(Stringpassword,doubleamount){verify(password);if(amount>balance)thrownewIllegalArgumentException("余额不足");balance-=amount;resetFailCount();}// 私有方法:密码验证privatevoidverify(Stringinput){if(locked)thrownewIllegalStateException("账户已锁定");if(!this.password.equals(input)){failCount++;if(failCount>=MAX_FAIL)locked=true;thrownewSecurityException("密码错误");}}privatevoidresetFailCount(){failCount=0;}}

💡生产环境进阶:上述balance在多线程下不安全。实际项目中可用AtomicLongsynchronized

privatefinalAtomicLongbalance=newAtomicLong();publicvoiddeposit(longamount){balance.addAndGet(amount);}

使用示例

BankAccountacc=newBankAccount("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;privatefinalList<String>tags;publicImmutablePerson(Stringname,List<String>tags){this.name=name;// 防御性拷贝:不信任外部传入的集合this.tags=List.copyOf(tags);// Java 9+,返回不可变集合}publicStringgetName(){returnname;}publicList<String>getTags(){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)可以公开,因为它是不可变的:

publicstaticfinaldoublePI=3.1415926;// 公开没问题

八、面试高频题(背下来)

Q1:封装是什么?为什么要封装?

:把数据和操作捆绑,隐藏内部细节。目的:①保护数据完整性 ②隔离变化 ③控制权限 ④降低耦合。

Q2:有 getter/setter 就算封装吗?

:不算。那是“贫血模型”。真正封装应该提供业务方法,比如withdraw()而不是setBalance()

Q3:反射能破坏封装吗?

:能。setAccessible(true)可以绕过 private。但这是框架层面的后门,业务代码不应使用。封装防的是粗心,不是恶意。

Q4:封装影响性能吗?

:现代 JVM 的 JIT 编译器会对 getter/setter 进行内联优化,最终性能与直接访问字段几乎无差别。不要为了微小的性能牺牲设计。

Q5:什么是防御性拷贝?

:返回内部可变对象(Date、List)时,返回副本或只读视图,防止外部修改内部状态。

Q6:Collections.unmodifiableList就绝对安全了吗?

:不是。它只防止增删改结构,但如果列表中的元素本身是可变的,外部仍可修改元素内容。需要元素也是不可变对象,或返回深拷贝。

九、思考题(含深度解析)

题1:下面代码有什么问题?

publicclassOrder{publicList<Item>items=newArrayList<>();publicvoidaddItem(Itemitem){items.add(item);}}

答案

  1. 属性是public,外部可以直接order.items.clear()破坏数据
  2. 应改为private,并提供getItems()返回Collections.unmodifiableList(items)
  3. 注意: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/5+32;}publicvoidsetCelsius(doublec){if(c<ABSOLUTE_ZERO)thrownewIllegalArgumentException("低于绝对零度");this.celsius=c;}publicvoidsetFahrenheit(doublef){setCelsius((f-32)*5/9);// 复用校验逻辑}}

题3(进阶):如何让一个类不能被继承且内部状态完全不可变?

答案

publicfinalclassImmutableConfig{privatefinalMap<String,String>settings;publicImmutableConfig(Map<String,String>settings){this.settings=Map.copyOf(settings);// 防御 + 不可变}publicMap<String,String>getSettings(){returnsettings;// 已经是不可变Map,直接返回安全}}

关键点:final class+final字段 + 构造器防御性拷贝 + 不提供修改方法 + 返回不可变视图。

十、总结

境界特征
青铜会用private+get/set
白银会在 setter 里写校验
黄金提供业务方法,不暴露内部数据
铂金会用不可变对象、防御性拷贝、包级私有
钻石理解模块化封装,能在封装与测试之间找到平衡

记住:好的封装,是让别人用你的类时,想出错都难。

http://www.jsqmd.com/news/654465/

相关文章:

  • Flash数字遗产的守护者:CefFlashBrowser如何让经典内容重获新生
  • OpenAI Chat Completion API 应用与使用指南
  • CAM++声纹识别系统案例分享:会议录音自动归档实战
  • 家庭游戏串流革命:用Sunshine打造你的私人云游戏服务器
  • STAR-CCM+内燃机缸内CFD仿真:从理论框架到代码实践
  • 聚焦重庆津诚青少年素质教育,满意度、案例及招生规模情况大揭秘 - 工业设备
  • 花岗岩路沿石定制厂家靠谱吗,有实力的厂家深度剖析 - 工业品网
  • 怎样在2024年完美运行Flash内容:现代用户的实用解决方案
  • 直播预告 | 密歇根州立大学刘思佳教授:从机器遗忘到更广泛的模型调控
  • SeqGPT-560M在卷积神经网络中的应用:图像文本联合分析
  • Nuitka 文件夹模块化打包
  • 2026年靠谱的车规级微控制器加工厂推荐,哪家售后好为你揭晓答案 - 工业品牌热点
  • Qwen3-14B RTX 4090D部署:TensorRT加速推理POC验证与性能对比
  • Wan2.1-UMT5进阶:利用LSTM时序模型优化视频连贯性
  • Python百度搜索API架构解析:无限制网页爬虫实现原理与性能优化
  • Fuchsia入门-简介和代码介绍
  • 飞书文档批量导出工具:一键备份团队知识资产
  • Pi0具身智能模型解释性分析与可视化工具使用指南
  • FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了
  • 基于Redis和Redisson实现分布式锁
  • 2026年多平台发布工具全攻略:10款高效自媒体管理软件深度评测与推荐
  • 5分钟掌握AMD Ryzen硬件调试:SMUDebugTool终极指南
  • Qwen3.5-9B Proteus仿真结合:为嵌入式项目生成说明文档与测试脚本
  • 职场真相:为何“会说”比“会做”更关键?这3件事,领导不问也得主动说
  • 细聊车规级MCU芯片制造厂哪家好,性价比与售后综合分析 - 工业推荐榜
  • ScriptCat中GM.xmlHttpRequest异步Promise机制深度解析与架构设计优化
  • iPhone充电慢怎么办?6个方法大幅缩短充电时间!
  • 从零构建RenderDoc扩展插件:打造自定义调试界面
  • Equalizer APO完整指南:免费打造Windows系统级音频均衡器
  • Zotero SciPDF插件:3分钟实现学术文献PDF自动下载的终极方案