摘要本文讲解了Java面向对象编程中的核心概念——封装Encapsulation。通过生动的比喻和丰富的代码示例详细介绍了如何使用访问修饰符private、public等将类的属性隐藏起来并通过公共的getter和setter方法进行安全访问。文章不仅阐述了封装的基本原理和JavaBean规范还探讨了封装与Record类的选择并通过综合实例展示了封装如何提升代码的安全性、可维护性和扩展性。关键词Java封装, 访问修饰符, getter和setter, JavaBean规范, 面向对象编程上篇文章我们理解了“类”和“对象”——学会了如何用设计图类造出具体的实物对象。但我们当时的设计有一个“隐患”所有属性都是直接暴露的。你可以随意访问和修改一个对象的name、age甚至把它改成任何不合理的值。比如说我们定义了一个Student类它有age属性。你完全可以把age设为-5或1500这在语法上是合法的但在现实中是完全不合理的。你会说“谁会把年龄设为负数呢”确实正常情况不会。但错误往往是意外的而不是故意的。一个好的程序应该能够在源头阻止不合理的数据进入系统而不是依赖使用者“自觉”。这就是我们今天要讲的主题 ——封装Encapsulation。1. 封装是什么—— 把“内部”和“外部”隔开封装字面上理解就是“装进一个匣子里封起来”。在面向对象编程中它的含义是把对象的属性和操作属性的方法绑定在一起并对外部隐藏内部实现的细节。说得更直白一点属性数据应该对“外界”不可见不能直接访问只能通过方法来访问和修改。方法的内部实现对外部也是不可见的外部只需要知道“调用这个方法能达到什么效果”而不需要知道“它是怎么做到的”。打个比方你开一台自动洗衣机。你只需要知道“按这个按钮是标准洗按那个按钮是快速洗”你不需要知道洗衣机内部的水泵怎么工作、电机怎么转动、程序怎么控制时间。洗衣机把内部复杂性封装起来了只给你暴露几个简单的按钮即方法。如果洗衣机的电路板直接暴露在外面任你触摸那不仅不安全而且你也无从下手。这就是封装的核心思想简化使用保护安全。2. 访问修饰符 —— 控制“可见性”的大门Java 通过访问修饰符Access Modifiers来实现封装。它们决定了“谁能访问这个成员字段或方法”。修饰符同一个类同一个包不同包的子类任何地方private✅❌❌❌default不写✅✅❌❌protected✅✅✅❌public✅✅✅✅我们一个一个来看。private—— 最严格的“隐私保护”用private修饰的成员只能在当前类内部访问。外部任何地方都不能直接访问它。这是我们实现封装最常用的修饰符。通常我们把所有属性都设为private然后通过public的方法来间接访问它们。publicclassStudent{privateStringname;// 私有属性外部不能直接访问privateintage;// 通过公共方法访问publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.namename;}}public—— 完全公开public修饰的成员任何地方都能访问。我们通常把方法设为public作为对外提供的“服务入口”。default包级私有 —— 同一包内的“朋友”如果不写任何修饰符就是default也叫“包级私有”。它表示只有同一个包package内的类可以访问。这个用得相对较少。protected—— 留给“子类”的权限protected表示“同一个包内 不同包的子类”可以访问。它主要用在继承关系中我们后面会详细讲。3. 实践给Student加上封装让我们把之前那个完全暴露的Student类改造一下加上封装publicclassStudent{// 所有属性都设为 privateprivateStringname;privateintage;privateStringstudentId;// ---- 构造方法 ----publicStudent(Stringname,intage,StringstudentId){// 在构造方法中我们也可以使用 setter 来做校验setName(name);setAge(age);setStudentId(studentId);}// ---- Getter 方法 ----publicStringgetName(){returnname;}publicintgetAge(){returnage;}publicStringgetStudentId(){returnstudentId;}// ---- Setter 方法 ----publicvoidsetName(Stringname){// 可以加一些基本校验if(namenull||name.trim().isEmpty()){System.out.println(警告姓名不能为空);this.name未知;}else{this.namename;}}publicvoidsetAge(intage){// 校验年龄的合理性if(age0||age150){System.out.println(警告年龄 age 不合理已设为 0。);this.age0;}else{this.ageage;}}publicvoidsetStudentId(StringstudentId){if(studentIdnull||studentId.trim().isEmpty()){System.out.println(警告学号不能为空);this.studentId未知;}else{this.studentIdstudentId;}}// ---- 业务方法 ----publicvoidsayHello(){System.out.println(你好我是 name今年 age 岁学号 studentId);}}现在外部代码不能再随意赋值了publicclassMain{publicstaticvoidmain(String[]args){Students1newStudent(张三,20,2024001);s1.sayHello();// 输出你好我是 张三今年 20 岁学号 2024001// 尝试设置不合理的数据s1.setAge(-5);// 会输出警告并把 age 设为 0s1.setName();// 会输出警告并把 name 设为 未知s1.sayHello();// 输出你好我是 未知今年 0 岁学号 2024001}}你看封装让我们的类变得“有脑子”了——它能够主动保护自己的数据拒绝不合理的信息而不是傻傻地接受任何值。4. 封装不仅仅是“私有化” —— 更高层的含义① 把变化隔离如果某个属性的存储方式发生变化比如把age从int改为String存储只要getAge()和setAge()的方法签名不变外部调用者完全不需要修改任何代码。变化被隔离在了类内部。② 提供“只读”或“只写”的权限有时候你希望某个属性只能读不能改。那你只需要提供getter不提供setter。privateStringid;// ID 一旦生成就不能修改publicStringgetId(){returnid;}// 没有 setter外部无法修改 id③ 在方法内部做额外的逻辑除了参数校验你还可以在 getter/setter 中做更多的事情比如记录日志谁在什么时候读取或修改了某个属性触发其他事件修改了价格自动重新计算总价延迟加载从数据库读取5. JavaBean 规范 —— 一种“约定俗成”的写法在 Java 开发中尤其是 Spring 等框架中你经常会听到JavaBean这个词。它不是一个类也不是一种技术而是一套编码规范类是public的。所有属性都是private的。每个属性都有对应的publicgetter 和 setter 方法除非该属性是只读的。有一个无参构造方法即使有有参构造也建议保留无参构造。实现Serializable接口可选用于序列化。我们平时的 POJOPlain Old Java Object普通 Java 对象类就是按照这套规范写的。publicclassUserimplementsSerializable{privateStringusername;privateStringpassword;privateintage;publicUser(){}// 无参构造publicUser(Stringusername,Stringpassword,intage){this.usernameusername;this.passwordpassword;this.ageage;}publicStringgetUsername(){returnusername;}publicvoidsetUsername(Stringusername){this.usernameusername;}publicStringgetPassword(){returnpassword;}publicvoidsetPassword(Stringpassword){this.passwordpassword;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.ageage;}}这套规范的优点是什么框架能够通过反射机制识别你的类结构自动完成很多工作比如序列化、数据绑定、JSON 转换等。如果你不遵守这个规范很多框架功能就无法生效。6. 封装与Record—— 新方式与旧方式的选择你可能会想“我们现在写的 getter/setter 好啰嗦啊那上一篇学的Record不香吗”确实Record自动生成了构造方法、访问器name()、age()和toString()但它不生成 setter因为 Record 是不可变的。两者各有适用场景场景推荐数据只读不需要修改✅Record简洁优雅数据需要修改有业务逻辑校验✅ 普通类 封装private getter/setter需要兼容框架如 Spring普通类更常见但 Record 也逐渐被支持在初学阶段我建议你把普通类的封装写法练熟因为它在实际工作中仍然是主流。等你熟练之后再根据场景灵活选择 Record。7. 一个综合例子带封装的商品类我们来写一个稍微复杂的例子——一个Product商品类包含封装、构造方法、业务方法publicclassProduct{// ---- 私有属性 ----privateStringid;privateStringname;privatedoubleprice;privateintstock;// 库存数量// ---- 构造方法 ----publicProduct(Stringid,Stringname,doubleprice,intstock){this.idid;setName(name);setPrice(price);setStock(stock);}// ---- Getter 方法 ----publicStringgetId(){returnid;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}publicintgetStock(){returnstock;}// ---- Setter 方法带校验 ----publicvoidsetName(Stringname){if(namenull||name.trim().isEmpty()){thrownewIllegalArgumentException(商品名称不能为空);}this.namename;}publicvoidsetPrice(doubleprice){if(price0){thrownewIllegalArgumentException(商品价格不能为负数);}this.priceprice;}publicvoidsetStock(intstock){if(stock0){thrownewIllegalArgumentException(库存数量不能为负数);}this.stockstock;}// ---- 业务方法 ----/** * 减少库存发货时调用 */publicvoidreduceStock(intquantity){if(quantity0){System.out.println(❌ 减少数量必须大于 0);return;}if(stockquantity){System.out.println(❌ 库存不足当前库存stock需要quantity);return;}stock-quantity;System.out.println(✅ 库存已减少 quantity剩余库存stock);}/** * 增加库存补货时调用 */publicvoidincreaseStock(intquantity){if(quantity0){System.out.println(❌ 增加数量必须大于 0);return;}stockquantity;System.out.println(✅ 库存已增加 quantity当前库存stock);}/** * 计算折扣价打 8 折 */publicdoublegetDiscountedPrice(doublediscountRate){if(discountRate0||discountRate1){System.out.println(❌ 折扣率必须在 0~1 之间);returnprice;}returnprice*(1-discountRate);}/** * 显示商品信息 */publicvoiddisplay(){System.out.println(商品IDid);System.out.println(商品名称name);System.out.println(商品价格price);System.out.println(库存数量stock);}}publicclassMain{publicstaticvoidmain(String[]args){ProductbooknewProduct(P001,Java编程思想,99.0,50);book.display();System.out.println(-------------------);// 尝试打折doublediscountedbook.getDiscountedPrice(0.2);System.out.println(8折后价格discounted);// 尝试减少库存book.reduceStock(10);// 库存减少 10book.reduceStock(100);// 库存不足报错// 尝试设置不合理的数据会抛异常try{book.setPrice(-10);}catch(IllegalArgumentExceptione){System.out.println(捕获异常e.getMessage());}}}这个例子展示了封装如何让一个类变得安全数据校验和自包含数据 操作数据的方法放在一起。8. 封装与面向对象的关系封装是面向对象三大特性中的第一个另外两个是继承和多态。它奠定了面向对象的基础没有了封装对象的属性可以被随意篡改数据完整性无法保证。没有了封装对象的内部实现对外部透明一旦实现改变所有依赖的代码都要修改。没有了封装对象就只是一个“数据袋子”而不是一个有“行为”和“职责”的实体。可以说封装是面向对象的“第一块砖”。今天的总结今天我们彻底理解了封装封装把数据和操作数据的方法绑在一起对外部隐藏内部细节。访问修饰符private最严格、default包级、protected继承相关、public完全公开。通常的做法属性用private方法用public。Getter/Setter 不只是“读取/设置”它们提供了校验、隔离变化、控制权限的机会。JavaBean 规范private 属性 public getter/setter 无参构造。封装让代码更安全、更可维护、更易扩展。现在你写的类已经有模有样了。它不再是一堆数据的堆砌而是一个能自我保护的“小实体”。动手试试把上篇文章的Book类改造成符合封装规范的版本所有属性private提供 getter/setter在 setter 中加入合理的校验价格不能为负、书名不能为空等。设计一个BankAccount银行账户类包含accountNumber、balance属性。提供deposit(double amount)和withdraw(double amount)方法确保余额不能为负取款不能超出余额。写一个Temperature类内部用double celsius存储温度。提供getCelsius()、setCelsius()、getFahrenheit()、setFahrenheit()方法实现摄氏度和华氏度的自动转换。公式F C * 9/5 32挑战结合第 6 篇的“名片生成器”和第 17 篇的Person类把Person类的属性改为private在 setter 中加入校验年龄 0~150姓名非空并重构printCard()方法直接使用私有属性。多写几个封装的类你会越来越习惯“把数据藏起来”的思维方式。这种“藏”不是不让人用而是让人通过正规的、受控的途径去用。我们下一篇见。 获取本系列示例代码请访问 GitCode。