1. 封装的定义
封装 是面向对象编程(OOP)的基础原则之一。通俗地说,封装就是把对象的内部细节(它的“内脏”)藏起来,只通过专门设计好的“门”——公共方法——来访问。
想象一台现代咖啡机。用户只看到按钮和显示屏——他不需要知道锅炉、泵和内部管路如何工作。他按下“卡布奇诺”——就得到结果。内部的一切都被隐藏了。这就是封装!
在 Java(以及其他 OOP 语言)中,封装通过以下方式实现:
- 数据隐藏——将类的字段声明为 private(至少不要是 public)。
- 公共接口——只“暴露”那些对象用户真正需要的方法。
示意图:封装长什么样
+-------------------------------+
| Class Student |
|-------------------------------|
| - name: String | // private 字段
| - age: int | // private 字段
|-------------------------------|
| + getName(): String | // public 方法
| + setName(String): void | // public 方法
| + getAge(): int | // public 方法
| + setAge(int): void | // public 方法
+-------------------------------+
这里的 - 表示 private(隐藏),而 + 表示 public(对外可见)。
什么是 getter 和 setter?
在讨论为什么需要封装之前,先快速认识一下 getter 和 setter——它们是与类的私有字段“交流”的专用方法。
Getter——获取私有字段值的方法。通常命名为 getFieldName()。
Setter——设置私有字段值的方法。通常命名为 setFieldName(value)。
简单示例:
public class Student {
private String name; // 私有字段——外部不可见
// Getter —— “把学生的名字给我”
public String getName() {
return name;
}
// Setter —— “设置学生的名字”
public void setName(String name) {
this.name = name;
}
}
如何使用:
Student student = new Student();
student.setName("Vasya"); // 通过 setter 设置名字
String name = student.getName(); // 通过 getter 获取名字
把 getter 和 setter 想象成向对象提出的“礼貌请求”:与其直接往它口袋里掏东西(student.name = "Vasya"),不如有礼貌地请求:“请设置名字”(student.setName("Vasya"))。
提前剧透:再过几讲我们会系统学习 getter 和 setter,了解它们的技巧并熟练运用。现在只需理解核心思想即可!
2. 为什么需要封装?
保护数据不被不当使用
如果类的所有字段都是 public,任何外部代码都能随意修改它们:
Student s = new Student();
s.age = -1000; // 哎呀,吸血鬼学生!
这很危险!程序可能表现出不可预测的行为,bug 会在最意想不到的地方冒出来。
在不影响外部代码的情况下修改内部实现
封装允许你修改类的内部实现,而不破坏它的使用方代码。例如,你可以改变数据的存储方式,或在方法中加入校验,类的使用者不会察觉——他们仍然调用相同的方法。
提升代码的可读性与可维护性
当内部细节被隐藏后,对外接口会更简洁、更清晰。使用你类的程序员无需理解内部工作原理——只需知道有哪些方法可用以及它们的作用。
生活中的例子
回想你如何使用智能手机。你不会去想屏幕触摸如何被处理、电池如何构造、通信模块如何工作。你只通过清晰的接口(图标、按钮)调用所需功能。哪怕厂商更换了内部实现,你也不会察觉。
代码中的真实示例
假设我们有一个类 BankAccount。旧版本程序把余额存成用点分隔的字符串,比如 "1.000.50"。后来程序员决定把余额存成数字 double。如果这个字段是公开的,那么所有直接访问 account.balance 的旧代码都会崩掉。
但如果我们使用封装,隐藏字段,只提供 deposit() 和 getBalance() 方法,外部代码甚至不会知道发生了变更:
public class BankAccount {
private double balance; // 字段被隐藏
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
现在如果明天我们想把余额按“分”来存(例如用 long),只需修改类的内部实现,所有调用 deposit() 和 getBalance() 的其他代码都会像以前一样正常工作。
3. 封装的反例与正例
反例:公开字段
public class Student {
public String name;
public int age;
}
这种做法的问题:
- 任何代码都能给字段赋任意值,甚至是无效值。
- 无法加入数据校验。
- 如果你决定修改字段的类型或结构,所有使用它的代码都得改。
正例:私有字段与公共方法
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
// 可以添加校验!
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("名字不能为空");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 确保年龄非负
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
this.age = age;
}
}
优点:
- 外部代码不能直接修改字段——只能通过方法。
- 可以添加校验、日志、自动行为(例如更新统计信息)。
- 即使内部表示发生变化(例如用另一种格式存年龄),对外接口依然不变。
使用时的样子
Student s = new Student();
s.setName("Alisa");
s.setAge(20);
System.out.println(s.getName() + ",年龄:" + s.getAge());
试着把年龄设为负数——你会在运行期立刻得到错误!你的程序能避免低级失误。
4. 与其他 OOP 原则的关系
封装是其他 OOP 原则的“母体”。没有它,就不会有继承、多态和抽象。我们稍后会详细学习它们,这里先简单提一下:
- 继承(extends)允许基于已有类创建新类,扩展或修改其行为。如果一个类的内部细节是开放的,子类可能会不小心破坏关键部分。
- 多态(不同类的对象对同一消息作出不同响应的能力)离不开对内部实现与外部接口的清晰分离。
- 抽象——只保留对象的重要特征并隐藏细节。封装有助于把抽象落地实现。
类比
想象一辆汽车。驾驶者只接触方向盘、踏板和手柄——这是接口。其他一切(发动机、变速箱、电子设备)都隐藏在引擎盖之下。如果驾驶者能直接操纵发动机的每个螺丝,事故会频繁得多!
5. 实战示例:在我们的应用中使用封装
让我们继续扩展教学应用——比如“通讯录”。假设有一个 Contact 类,存储姓名和电话。
没有封装(反例):
public class Contact {
public String name;
public String phone;
}
使用方式:
Contact contact = new Contact();
contact.name = ""; // 哎呀!名字是空的
contact.phone = null; // 电话未设置
有封装(正确做法):
public class Contact {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("联系人姓名不能为空");
}
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new IllegalArgumentException("电话不能为空");
}
this.phone = phone;
}
}
现在外部代码不能留下空名字或空电话:
Contact contact = new Contact();
contact.setName("伊万");
contact.setPhone("+1-999-123-45-67");
如果试图设置空名字——程序会抛出错误。
6. 有用的细节
封装与长期维护
在小型教学项目里,你可能觉得“凭良心写代码”也行:谁会把年龄设为负数或名字设为空呢?但一旦项目变大、有了其他开发者,甚至你自己过几个月也会忘了实现细节——这时封装就能拯救混乱。
- 更容易修改类的内部——如果需要把电话存为 PhoneNumber 对象而不是字符串,你只需修改实现,外部代码无需改动。
- 更易测试——如果所有修改都通过方法进行,就容易追踪何时修改了哪些数据。
- 更少 bug——防止无效值和意外改动。
问题:是否总是需要 getter 和 setter?
很多初学者以为:“既然封装就是私有字段配公共 getter/setter,那每个字段都要配 getter 和 setter!”并不总是如此。
- 有时字段应当只读(例如对象的唯一标识)。那就只提供 getter。
- 有的字段根本不需要“对外暴露”——那就不要提供 getter 或 setter。
- 如果字段只能在类内部修改,可以把 setter 设为私有的。
黄金法则:只开放外部代码真正需要的数据和方法。
可视化:方法对比
| 方式 | 字段访问示例 | 可控性 | 安全性 |
|---|---|---|---|
| public 字段 | |
否 | 低 |
| private 字段 + 方法 | |
是 | 高 |
7. 使用封装时的常见错误
错误 № 1:把类的所有字段都声明为 public。 这是初学者最常见的错误。这类代码很快会失控:任何人都能在你不知情的情况下修改任何数据。不要这么做——哪怕你很想省时间!
错误 № 2:没有校验与逻辑的 getter 和 setter。 既然你提供了访问方法,就利用它们做校验:不要允许赋予无效值。简单地把参数值“原样”拷贝到字段里并不总是最佳做法。
错误 № 3:过早暴露内部结构。 如果你“以防万一”给所有字段都提前做了 getter/setter,你就有可能暴露过多细节,后续会很难修改。
错误 № 4:直接返回可变对象。 如果字段是可变对象(比如列表),不要通过 getter 直接返回它。最好返回副本,或让其不可变。
GO TO FULL VERSION