摘要: 本文讲解了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 的方法来间接访问它们。
public class Student {
private String name; // 私有属性,外部不能直接访问
private int age;
// 通过公共方法访问
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public —— 完全公开
public 修饰的成员任何地方都能访问。我们通常把方法设为 public,作为对外提供的“服务入口”。
default(包级私有) —— 同一包内的“朋友”
如果不写任何修饰符,就是 default,也叫“包级私有”。它表示只有同一个包(package) 内的类可以访问。这个用得相对较少。
protected —— 留给“子类”的权限
protected 表示“同一个包内 + 不同包的子类”可以访问。它主要用在继承关系中,我们后面会详细讲。
3. 实践:给 Student 加上封装
让我们把之前那个完全暴露的 Student 类改造一下,加上封装:
public class Student {
// 所有属性都设为 private
private String name;
private int age;
private String studentId;
// ---- 构造方法 ----
public Student(String name, int age, String studentId) {
// 在构造方法中,我们也可以使用 setter 来做校验
setName(name);
setAge(age);
setStudentId(studentId);
}
// ---- Getter 方法 ----
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getStudentId() {
return studentId;
}
// ---- Setter 方法 ----
public void setName(String name) {
// 可以加一些基本校验
if (name == null || name.trim().isEmpty()) {
System.out.println("警告:姓名不能为空!");
this.name = "未知";
} else {
this.name = name;
}
}
public void setAge(int age) {
// 校验年龄的合理性
if (age < 0 || age > 150) {
System.out.println("警告:年龄 " + age + " 不合理,已设为 0。");
this.age = 0;
} else {
this.age = age;
}
}
public void setStudentId(String studentId) {
if (studentId == null || studentId.trim().isEmpty()) {
System.out.println("警告:学号不能为空!");
this.studentId = "未知";
} else {
this.studentId = studentId;
}
}
// ---- 业务方法 ----
public void sayHello() {
System.out.println("你好,我是 " + name + ",今年 " + age + " 岁,学号 " + studentId);
}
}
现在,外部代码不能再随意赋值了:
public class Main {
public static void main(String[] args) {
Student s1 = new Student("张三", 20, "2024001");
s1.sayHello(); // 输出:你好,我是 张三,今年 20 岁,学号 2024001
// 尝试设置不合理的数据
s1.setAge(-5); // 会输出警告,并把 age 设为 0
s1.setName(""); // 会输出警告,并把 name 设为 "未知"
s1.sayHello(); // 输出:你好,我是 未知,今年 0 岁,学号 2024001
}
}
你看,封装让我们的类变得“有脑子”了——它能够主动保护自己的数据,拒绝不合理的信息,而不是傻傻地接受任何值。
4. 封装不仅仅是“私有化” —— 更高层的含义
① 把变化隔离
如果某个属性的存储方式发生变化(比如把 age 从 int 改为 String 存储),只要 getAge() 和 setAge() 的方法签名不变,外部调用者完全不需要修改任何代码。变化被隔离在了类内部。
② 提供“只读”或“只写”的权限
有时候,你希望某个属性只能读,不能改。那你只需要提供 getter,不提供 setter。
private String id; // ID 一旦生成就不能修改
public String getId() {
return id;
}
// 没有 setter,外部无法修改 id
③ 在方法内部做额外的逻辑
除了参数校验,你还可以在 getter/setter 中做更多的事情,比如:
- 记录日志(谁在什么时候读取或修改了某个属性)
- 触发其他事件(修改了价格,自动重新计算总价)
- 延迟加载(从数据库读取)
5. JavaBean 规范 —— 一种“约定俗成”的写法
在 Java 开发中,尤其是 Spring 等框架中,你经常会听到 JavaBean 这个词。它不是一个类,也不是一种技术,而是一套编码规范:
- 类是
public的。 - 所有属性都是
private的。 - 每个属性都有对应的
publicgetter 和 setter 方法(除非该属性是只读的)。 - 有一个无参构造方法(即使有有参构造,也建议保留无参构造)。
- 实现
Serializable接口(可选,用于序列化)。
我们平时的 POJO(Plain Old Java Object,普通 Java 对象)类,就是按照这套规范写的。
public class User implements Serializable {
private String username;
private String password;
private int age;
public User() {} // 无参构造
public User(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
这套规范的优点是什么?框架能够通过反射机制识别你的类结构,自动完成很多工作(比如序列化、数据绑定、JSON 转换等)。如果你不遵守这个规范,很多框架功能就无法生效。
6. 封装与 Record —— 新方式与旧方式的选择
你可能会想:“我们现在写的 getter/setter 好啰嗦啊,那上一篇学的 Record 不香吗?”
确实,Record 自动生成了构造方法、访问器(name()、age())和 toString(),但它不生成 setter,因为 Record 是不可变的。
两者各有适用场景:
| 场景 | 推荐 |
|---|---|
| 数据只读,不需要修改 | ✅ Record,简洁优雅 |
| 数据需要修改,有业务逻辑校验 | ✅ 普通类 + 封装(private + getter/setter) |
| 需要兼容框架(如 Spring) | 普通类更常见,但 Record 也逐渐被支持 |
在初学阶段,我建议你把普通类的封装写法练熟,因为它在实际工作中仍然是主流。等你熟练之后,再根据场景灵活选择 Record。
7. 一个综合例子:带封装的商品类
我们来写一个稍微复杂的例子——一个 Product(商品)类,包含封装、构造方法、业务方法:
public class Product {
// ---- 私有属性 ----
private String id;
private String name;
private double price;
private int stock; // 库存数量
// ---- 构造方法 ----
public Product(String id, String name, double price, int stock) {
this.id = id;
setName(name);
setPrice(price);
setStock(stock);
}
// ---- Getter 方法 ----
public String getId() {
return id;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public int getStock() {
return stock;
}
// ---- Setter 方法(带校验) ----
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("商品名称不能为空");
}
this.name = name;
}
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("商品价格不能为负数");
}
this.price = price;
}
public void setStock(int stock) {
if (stock < 0) {
throw new IllegalArgumentException("库存数量不能为负数");
}
this.stock = stock;
}
// ---- 业务方法 ----
/**
* 减少库存(发货时调用)
*/
public void reduceStock(int quantity) {
if (quantity <= 0) {
System.out.println("❌ 减少数量必须大于 0");
return;
}
if (stock < quantity) {
System.out.println("❌ 库存不足!当前库存:" + stock + ",需要:" + quantity);
return;
}
stock -= quantity;
System.out.println("✅ 库存已减少 " + quantity + ",剩余库存:" + stock);
}
/**
* 增加库存(补货时调用)
*/
public void increaseStock(int quantity) {
if (quantity <= 0) {
System.out.println("❌ 增加数量必须大于 0");
return;
}
stock += quantity;
System.out.println("✅ 库存已增加 " + quantity + ",当前库存:" + stock);
}
/**
* 计算折扣价(打 8 折)
*/
public double getDiscountedPrice(double discountRate) {
if (discountRate < 0 || discountRate > 1) {
System.out.println("❌ 折扣率必须在 0~1 之间");
return price;
}
return price * (1 - discountRate);
}
/**
* 显示商品信息
*/
public void display() {
System.out.println("商品ID:" + id);
System.out.println("商品名称:" + name);
System.out.println("商品价格:¥" + price);
System.out.println("库存数量:" + stock);
}
}
public class Main {
public static void main(String[] args) {
Product book = new Product("P001", "Java编程思想", 99.0, 50);
book.display();
System.out.println("-------------------");
// 尝试打折
double discounted = book.getDiscountedPrice(0.2);
System.out.println("8折后价格:¥" + discounted);
// 尝试减少库存
book.reduceStock(10); // 库存减少 10
book.reduceStock(100); // 库存不足,报错
// 尝试设置不合理的数据(会抛异常)
try {
book.setPrice(-10);
} catch (IllegalArgumentException e) {
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。

469

被折叠的 条评论
为什么被折叠?



