Day14 --【Java全栈】 面向对象编程[2]封装
面向对象三大核心特性
封装、继承、多态,是面向对象的三大支柱,也是面试 100% 会问到的核心。
思考:如果类里的属性、方法全都对外公开,所有人想改就直接改,会出现什么问题?
全部公开 = 数据没人管、逻辑随便破、改一动全身、出 bug 查不到、想优化无从下手。
那么问题怎么解决呢?这个时候就需要用到封装
封装(Encapsulation)
一、 定义
封装是面向对象的核心思想之一:将对象的内部状态(属性)和内部实现(方法)隐藏起来,对外只暴露有限的、可控的访问入口,所有对内部数据的修改都必须通过指定入口完成。
核心本质不是「完全不让访问」,而是「可控访问」——把数据和逻辑的控制权牢牢握在类自己手里,外部只能通过我们允许的方式操作。
💡 通俗理解
如果奶茶店没有封装,操作间全透明,顾客可以自己伸手进机器改糖度、自己拿珍珠、自己改价格,会发生什么?
有人乱改配方,做出来的奶茶口味全乱了;
有人直接改价格,12块的奶茶改成1块钱,店铺亏钱;
制作步骤随便改,今天煮茶3分钟、明天煮10分钟,品质完全不稳定。
封装就是给操作间装上玻璃墙、加上门禁:
顾客看不到里面的配料比例、制作步骤,也碰不到原料;
想喝奶茶只能通过「前台点单」这个唯一正规入口,说清楚口味、甜度;
内部怎么制作、用什么配方、加多少料,全由奶茶店自己把控,顾客不用管,也改不了。
二、封装的三大核心价值
1. 保护数据安全,杜绝非法值
比如奶茶价格不能为负数、甜度只能选3/5/7/10分、库存不能小于0。如果属性直接对外暴露,外部随便赋值,很容易产生脏数据;通过方法入口就能加校验,保证数据永远合法。
更深一层,封装还能保证对象内部多个属性之间的状态一致性。比如「订单状态」和「支付时间」必须同步变化:状态变为已支付时,支付时间也要同步赋值。如果两个属性直接对外暴露,就可能出现只改了状态、忘了更新时间的矛盾脏数据。封装可以通过一个统一的行为方法一次性修改所有相关属性,从根源上杜绝这种状态不一致的问题。
2. 降低代码耦合,内部修改不影响外部
只要对外的入口(方法名、参数)不变,内部怎么改逻辑、怎么优化实现,外部调用者完全感知不到,也不用改代码。比如奶茶制作步骤升级了,顾客还是只要说「做一杯珍珠奶茶」就行,不用关心内部流程变了。
3. 简化使用成本,隐藏实现复杂度
调用者不用知道内部有多少步骤、多少细节,只要调用对外的方法就能完成功能。比如你不用知道奶茶怎么做,只要点单就能拿到成品。
三、Java 实现封装的基石:访问控制修饰符
既然封装的核心是「可控访问」,那Java靠什么来控制不同范围的访问权限呢?答案就是四类访问控制修饰符。
封装的底层,就是靠这四类访问权限来控制「谁能访问、谁不能访问」。
这四个修饰符可以修饰成员变量、成员方法、类,控制可见范围。
权限从小到大对比⬇️
修饰符 | 同类内 | 同包下 | 不同包的子类 | 所有地方 | 奶茶店类比 | 核心使用场景 |
private(私有) | ✅ 可访问 | ❌ 不可访问 | ❌ 不可访问 | ❌ 不可访问 | 操作间里的原料配比、秘制配方,只有奶茶师自己能碰 | 成员变量、内部工具方法,彻底隐藏内部细节 |
default(包私有,不写就是它) | ✅ 可访问 | ✅ 可访问 | ❌ 不可访问 | ❌ 不可访问 | 同一家门店的所有员工都能进的后厨区,外店人进不来 | 同包内复用的工具类、辅助方法 |
protected(受保护) | ✅ 可访问 | ✅ 可访问 | ✅ 可访问 | ❌ 不可访问 | 总店配方,所有分店的奶茶师都能用,外部顾客不行 | 专门留给子类重写、复用的方法 |
public(公共) | ✅ 可访问 | ✅ 可访问 | ✅ 可访问 | ✅ 可访问 | 前台点单窗口,所有人都能来下单 | 对外暴露的正式方法、类的对外接口 |
🔴 记忆:default 和 protected 的唯一区别就体现在不同包的子类场景——protected 允许子类访问,default 不允许。
我们平时说的「属性私有化、方法公开化」,就是用
private修饰属性,用public修饰对外的访问方法,这是封装最经典、最常用的实现方式。详细可见➡️Day10 --【Java全栈】方法 (上)
代码示例:不同权限在奶茶类里的体现⬇️
public class MilkTea { // ====== 私有属性(private):彻底隐藏,外部不能直接碰 ====== private String taste; // 口味 private double price; // 价格 private String secretFormula; // 秘制配方,只有类内部能访问 // ====== 受保护方法(protected):同包+子类能用,给子类复用 ====== protected void boilTea() { System.out.println("煮茶底,按照" + secretFormula + "配方煮"); } // ====== 包私有方法(default,不写修饰符):同包下能用 ====== void addMilk() { System.out.println("加牛奶"); } // ====== 公共方法(public):对外暴露的唯一入口 ====== // 获取价格 public double getPrice() { return price; } // 设置价格,内置校验 public void setPrice(double price) { if(price < 0) { System.out.println("价格不能为负数,设置失败"); return; } this.price = price; } // 对外的制作方法,内部调用私有步骤 public void make() { boilTea(); addMilk(); System.out.println("制作完成:" + taste + "奶茶"); } }四、this关键词
1.定义
this是 Java 中的关键字,代表当前对象的引用,也就是指向当前正在执行的对象本身。它可以用来访问当前对象的属性、方法,也可以调用当前对象的其他构造方法。
顺着封装的场景往下想:我们写 set 方法时,为了见名知意,通常会把参数名和成员变量名写得一致(比如都叫
price),那程序怎么区分哪个是类的成员属性、哪个是方法的局部参数呢?
💡 通俗理解
this就代表「当前正在操作的这个对象自己」。
类比奶茶店:奶茶师手里正在制作的这杯奶茶,就是this。当他说「我的口味」「我的价格」时,指的就是当前这杯奶茶自己的属性,而不是别的奶茶、也不是原料。
一句话:哪个对象调用了方法,方法里的 this 就代表哪个对象。
2. 封装场景下this的三大核心用法
用法1:区分成员变量和局部变量(封装最核心、最常用)
这是我们写 get/set 方法时必用的用法,也是封装的基础。
为什么会有区分问题?
Java中有就近原则:方法里出现同名变量时,会优先使用离得更近的局部变量(方法参数)。如果直接写
price = price;,相当于参数自己给自己赋值,成员变量根本没被修改,封装的赋值逻辑就失效了。
比如⬇️
public class MilkTea { // ====== 私有属性(private):彻底隐藏,外部不能直接碰 ====== private String taste; // 口味 private double price; // 价格 private String secretFormula; // 秘制配方,只有类内部能访问 // 此时你要调用该方法设置价格 public void setPrice(double price) { // ❌ 没有 this:遵循就近原则,参数自己赋给自己,成员变量纹丝不动 price = price; //那么这个设置就没有任何的意义,并没有将价格设置成功 // ✅ 有 this:明确指定把参数的值赋给当前对象的成员变量 this.price = price; //此时才能真正的将价格设置成功 } }this.属性名就明确表示「我要访问的是当前对象的成员变量」,和局部变量彻底区分开。
💡 一句话:等号左边加
this的是对象的属性,右边是传进来的值。
用法2:调用本类的其他构造方法
不仅普通方法里能用this,构造方法之间也可以通过this(参数)互相调用,复用初始化逻辑,减少重复代码,让封装的初始化逻辑更统一。
public class MilkTea { private String taste; private double price; // 单参构造:调用全参构造,默认价格设为12元 //也就是当你创建一个新的实例时,你只传了taste参数,会默认的价格 public MilkTea(String taste) { this(taste, 12.0); } // 全参构造:完成所有属性的最终初始化 public MilkTea(String taste, double price) { this.taste = taste; this.price = price; } }必须遵守的两条规则
this(参数)只能写在构造方法的第一行,不能写在普通方法里禁止循环调用(A构造调B,B构造又调A),会直接编译报错
用法3:代表当前对象本身
this单独使用时,就代表当前对象的引用本身,可以当作返回值、方法参数传递。
最典型的应用场景是链式编程:每个 set 方法都返回this,就可以连续调用多个 set 方法,代码更简洁流畅。
极简链式调用示例⬇️
public MilkTea setTaste(String taste) { this.taste = taste; return this; // 返回当前对象本身 } public MilkTea setPrice(double price) { this.price = price; return this; }使用时可以一行代码设置多个属性:
// 链式调用,连续设置多个属性 MilkTea tea = new MilkTea().setTaste("珍珠").setPrice(12);这种写法在 Lombok 的@Accessors(chain = true)注解和建造者模式中非常常见,日常封装基础属性时了解即可。
3. 结合封装完整示例
我们把封装的完整代码拿出来,标注所有this的作用,对应到实际开发场景:
public class MilkTea { private String taste; private double price; // 口味的set方法:this.taste 代表成员变量,taste 代表方法参数 public void setTaste(String taste) { this.taste = taste; } // 价格的set方法:加校验,校验通过后用this赋值给成员变量 public void setPrice(double price) { if(price < 0) { System.out.println("价格不能为负数"); return; } this.price = price; } // 普通方法里也可以用this访问本类的属性和其他方法 public void make() { System.out.println("制作" + this.taste + "奶茶"); // this.调用本类私有方法,this.也可以省略不写 this.checkPrice(); } private void checkPrice() { System.out.println("价格校验通过:" + this.price); } }4.容易踩的坑
误区1:静态方法里能用 this
不记得什么是静态方法➡️Day11 --【Java全栈】方法 (下) 第5点
✅ 正解:static静态方法属于类,不属于某个具体对象;this代表当前对象,所以静态方法里不能用this,会直接编译报错。
误区2:变量名不一样就不用写 this,故意起不同名字更省事
✅ 正解:如果参数名和成员变量名不一样(比如参数叫newPrice),不会产生歧义,确实可以不用写this。
不过行业规范推荐故意写成一样的名字,再用this区分。因为setPrice(double newPrice)远不如setPrice(double price)直观——调用者一看方法名和参数,就知道这是给 price 属性赋值的方法,代码可读性更强。
误区3:this 就是对象本身
✅ 正解:this是对象的引用(内存地址),不是对象本体,和之前讲的「引用变量 ≠ 对象本体」是同一个逻辑。它指向对象,但不是对象本身。
5. 和封装的关联总结
封装的核心是「属性私有化,通过公共方法访问」,而this就是公共方法里操作私有属性的关键桥梁——没有this,我们就没法在同名参数下准确给成员变量赋值,封装的可控访问也就无从谈起。
可以说:
private负责把属性藏起来,this负责在内部精准操作属性,二者配合完成完整的封装逻辑。
五、封装的两大核心维度
维度1:属性封装(数据封装)—— 最基础、最常用
就是我们最熟悉的:属性用private私有化,对外提供public的get/set方法。
get方法(getter):读取属性值,一般只读,不修改数据;
set方法(setter):设置属性值,里面可以加校验、转换、日志等逻辑,保证数据合法。
为什么不直接把属性设为public?
如果直接写成public double price;,外部可以随便赋值,比如tea.price = -999;,系统就会出现价格为负的脏数据,而且你完全拦不住。
用set方法封装后,所有修改都必须走你的校验逻辑,从根源上避免非法数据。
💡 补充:不是所有属性都要同时有get和set
只允许读、不允许改的属性,只写get方法(比如订单创建时间,生成后就不能改);
只允许内部改、外部不能改的属性,只写set方法甚至都不写;
完全内部使用的属性,get和set都不用写。
维度2:方法封装(实现封装)—— 进阶、更体现设计思想
把复杂的内部执行步骤隐藏起来,只对外暴露一个简单的总入口方法。调用者只要调用这个方法就行,不用管里面分了多少步、每步怎么实现。
顾客只要说「做一杯珍珠奶茶」(调用public的make方法),不用管里面先煮茶、再加奶、再加珍珠、摇杯这些内部步骤;这些内部步骤都是private的,外部看不到、也调用不了。
public class MilkTea { private String taste; // 私有内部方法:外部不能调用,只有类内部自己用 private void boilTea() { System.out.println("1. 煮茶底"); } private void addIngredients() { System.out.println("2. 加" + taste + "配料"); } private void shakeCup() { System.out.println("3. 摇杯均匀"); } // 对外暴露的公共入口:只有这一个方法对外可见 public void makeMilkTea(String taste) { this.taste = taste; // 内部步骤全部封装起来,外部不用关心 boilTea(); addIngredients(); shakeCup(); System.out.println("奶茶制作完成!"); } }外部调用时,只需要一行代码:
public class Test { public static void main(String[] args) { MilkTea tea = new MilkTea(); // 只需要调用对外的方法,不用管内部三步怎么执行 tea.makeMilkTea("珍珠"); } }以后内部制作步骤升级了(比如多加一步「封口」),只要makeMilkTea这个入口不变,外部调用的代码一行都不用改,这就是封装带来的「低耦合」优势。
🟡 补充:包的封装作用
封装不仅体现在单个类内部,包(package)也是 Java 里的一级封装单位。用默认访问权限(不写修饰符,即 default)修饰的类或方法,可以被同一个包下的其他类访问,但包外的类完全看不到。这提供了一种比 public 更安全、比 private 更灵活的封装层级,适合在同一个业务组件内部共享实现细节,对外则隐藏内部实现。
六、实际开发中的封装落地规范
理解了封装的两层含义,那在日常项目开发中,封装有哪些通用的落地规范,可以让我们的代码更严谨、更易维护呢?
1. 实体类(POJO/DO)的封装规范
这是日常开发写得最多的,对应数据库表的实体类:
所有成员变量全部用
private修饰;为需要访问的属性提供
get/set方法;构造方法、toString、equals、hashCode等公共方法用
public;实际开发中一般用 Lombok 的
@Data注解一键生成,不用手写。
这种写法也是JavaBean 规范的要求:类必须是 public 的,拥有 public 的无参构造方法,属性通过 getXxx/setXxx 方法访问。遵守这个规范后,Spring、MyBatis 等框架才能通过反射自动读取和设置对象的属性值。
2. 业务层(Service)的封装规范
核心业务逻辑封装在 service 层的 public 方法里,controller 层只调用入口,不直接操作数据库;
内部的校验逻辑、计算逻辑、数据转换逻辑,抽成 private 的私有方法,只在类内部复用,不对外暴露。
3. 工具类的封装规范
工具类构造方法私有化(private),防止别人new对象;
所有工具方法用 public static 修饰,对外直接调用;
内部的辅助计算方法用 private 隐藏,只暴露最终可用的工具方法。
七、不熟悉的同学容易踩的5个认知误区
讲完了正确的实现方式和规范,我们也梳理几个常见的认知偏差,避免理解走偏。
误区1:封装就是写get/set方法
✅ 正解:get/set只是属性封装的一种实现方式,是封装的入门级用法。封装还包括方法实现的隐藏、访问权限的控制、内部逻辑的隔离,范围要大得多。
误区2:所有属性都必须写get和set
✅ 正解:按需暴露。只允许读的就只写get,完全内部用的就都不写。盲目给所有属性都加set方法,等于把数据的控制权又交出去了,失去了封装的意义。
误区3:封装就是私有化,藏得越深越好
✅ 正解:封装的核心是「可控」,不是「完全隐藏」。该对外暴露的入口要清晰、稳定,不该暴露的细节彻底藏起来,过度封装会让代码变得冗余难用。
准确地说,封装追求的是信息隐藏(Information Hiding)——隐藏的是易变的实现细节,暴露的是稳定的对外接口。实现细节可以随时优化调整,只要对外接口保持不变,就不会影响外部调用方的代码。
❌ 过度封装的典型反例:一个简单的属性赋值,set 方法里又嵌套调用了 5 层私有方法,层层跳转才能完成一次赋值,逻辑没有任何复用价值,反而大幅提升了代码的阅读成本——这就属于把简单问题复杂化的过度封装。
误区4:public方法里可以随便改内部属性
✅ 正解:哪怕是public方法,修改内部属性也要保证数据合法。不能因为是对外方法就跳过校验,所有修改内部状态的入口,都要遵守数据校验规则。
误区5:封装只是面向对象的语法技巧
✅ 正解:封装不只是语法,更是一种设计思想。小到一个类的属性方法,大到一个模块、一个系统的接口设计,核心思路都是「隐藏内部细节,对外暴露稳定入口」,是降低系统复杂度的核心手段。
八、和之前知识点的串联
1. 关联访问修饰符:封装的底层实现就是靠四类访问权限,通过权限控制实现「隐藏内部、暴露入口」。
2. 关联构造方法:构造方法也是封装的一部分——对象创建的初始化逻辑封装在构造方法里,外部只要传参数就能得到合法对象,不用关心内部赋值细节。
3. 关联设计原则:封装是「单一职责原则」「开闭原则」的基础——内部逻辑自己管、自己改,不影响外部,才能做到对扩展开放、对修改关闭。
ps:今天好多,本来想分两天的,算了,就这样吧