【Java】封装:你的数据不该被随意触碰
【Java】封装——语言根基(三)
封装:你的数据不该被随意触碰
从入门到面试,一篇就够了(真正完整版)
一、引言:一个“太开放”的惨痛教训
想象你开了一家银行,为了“方便客户”,你把金库的钥匙挂在门口,上面贴了张纸条:“请自行取用,用完放回”。
结果可想而知。这很荒唐,对吧?
但在编程世界里,很多初学者就在做同样荒唐的事:
BankAccount account = new BankAccount();
account.balance = -1000; // 余额变成负数!
account.owner = ""; // 账户名为空!
没有任何保护,数据可以被随意篡改成非法状态。
这就是为什么我们需要 封装。
二、封装是什么?
官方定义:将数据(属性)和操作数据的方法(行为)捆绑在一起,并隐藏内部实现细节,对外提供受控的访问接口。
一句话人话:把数据锁进保险柜,只开一个小窗口让外部操作。
┌─────────────────────┐
│ 外部调用方 │
│ 只能通过窗口操作 │
└──────────┬──────────┘
│
┌──────▼──────┐
│ public方法 │ ← 小窗口(可控)
├─────────────┤
│ private属性 │ ← 保险柜(隐藏)
│ private方法 │
└─────────────┘
三、为什么需要封装?
3.1 保护数据完整性
没有封装 → 数据可以被随意篡改:
// 没有封装
Student s = new Student();
s.age = -5; // 编译通过,但数据非法
有了封装 → 加校验,阻止非法数据:
// 有封装
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄非法");
}
this.age = age;
}
3.2 隐藏内部实现,允许未来修改
// 外部只调用 getFullName()
public String getFullName() {
return firstName + " " + lastName;
}
// 内部随便改,外部无感知
public String getFullName() {
return lastName + firstName; // 改成姓在前名在后
}
3.3 控制访问权限
public double getBalance() { return balance; } // 人人可看
public void deposit(double amount) { ... } // 人人可存
private void audit() { ... } // 只有内部能用
3.4 降低耦合
封装后,每个类是独立的黑盒。调用方只需要知道“能做什么”,不需要知道“怎么做的”。
四、怎么实现封装?
4.1 访问修饰符(一张表搞定)
| 修饰符 | 同类 | 同包 | 子类 | 任意 |
|---|---|---|---|---|
private |
✓ | × | × | × |
| 默认(不写) | ✓ | ✓ | × | × |
protected |
✓ | ✓ | ✓ | × |
public |
✓ | ✓ | ✓ | ✓ |
最佳实践:
- 属性:几乎总是
private(特例:public static final常量可以公开) - 方法:
public(对外)、private(内部辅助)、protected(给子类) - 包级私有(默认):用于同一包内的协作类,是一种被低估的封装手段
// package-private:同包可访问,对外隐藏
class InternalHelper {
void doPackageLevelWork() { ... }
}
4.2 Getter/Setter 不是万能的
坏习惯:给每个 private 字段无脑生成 getter/setter。
// 这样写等于把属性公开了,封装了个寂寞
public String getPassword() { return password; } // 暴露密码!
好习惯:只暴露真正需要的。
public class User {
private String password;
// 不提供 getter,只提供验证
public boolean checkPassword(String input) {
return this.password.equals(input);
}
// 修改需要原密码验证
public void changePassword(String oldPwd, String newPwd) {
if (checkPassword(oldPwd)) {
this.password = newPwd;
}
}
}
4.3 防御性拷贝(面试常考)
当返回可变对象时,不要直接返回内部引用:
// 危险:外部可以修改内部数据
public Date getBirthday() {
return this.birthday;
}
// 安全:返回副本
public Date getBirthday() {
return new Date(this.birthday.getTime());
}
// 集合也同理——注意:unmodifiableList 只防结构修改,不防元素修改
public List<Item> getItems() {
return Collections.unmodifiableList(items);
}
// 如需深度防御,元素也应为不可变对象或返回深拷贝
public List<Item> getItemsDeepCopy() {
return items.stream().map(Item::copy).collect(Collectors.toList());
}
4.4 业务方法 > Setter
// 暴露了内部结构
public void setItems(List<Item> items) { ... }
public void setTotalPrice(double price) { ... }
// 提供业务方法
public void addItem(Item item) {
items.add(item);
totalPrice += item.getPrice();
}
五、完整案例:安全的银行账户
public class BankAccount {
// 私有属性
private String accountNo;
private double balance; // 注意:生产环境需考虑线程安全
private String password;
private int failCount;
private boolean locked;
private static final int MAX_FAIL = 3;
// 构造方法
public BankAccount(String accountNo, String password, double initBalance) {
this.accountNo = accountNo;
this.password = password;
this.balance = initBalance;
}
// 只读属性(无setter)
public String getAccountNo() { return accountNo; }
// 查看余额需要密码
public double getBalance(String password) {
verify(password);
return balance;
}
// 存款(公开)
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("金额必须大于0");
balance += amount;
resetFailCount();
}
// 取款(需要密码)
public void withdraw(String password, double amount) {
verify(password);
if (amount > balance) throw new IllegalArgumentException("余额不足");
balance -= amount;
resetFailCount();
}
// 私有方法:密码验证
private void verify(String input) {
if (locked) throw new IllegalStateException("账户已锁定");
if (!this.password.equals(input)) {
failCount++;
if (failCount >= MAX_FAIL) locked = true;
throw new SecurityException("密码错误");
}
}
private void resetFailCount() { failCount = 0; }
}
💡 生产环境进阶:上述
balance在多线程下不安全。实际项目中可用AtomicLong或synchronized:private final AtomicLong balance = new AtomicLong(); public void deposit(long amount) { balance.addAndGet(amount); }
使用示例:
BankAccount acc = new BankAccount("123456", "1234", 1000);
acc.deposit(500); // 存款:不需要密码
acc.withdraw("1234", 200); // 取款:需要密码
System.out.println(acc.getBalance("1234")); // 1300
// acc.balance = -100; // 编译错误!private 不可访问
六、进阶:封装在真实项目中的样子
6.1 不可变对象(真正的终极封装)
不可变对象天然线程安全,是最彻底的封装形式:
public final class ImmutablePerson {
private final String name;
private final List<String> tags;
public ImmutablePerson(String name, List<String> tags) {
this.name = name;
// 防御性拷贝:不信任外部传入的集合
this.tags = List.copyOf(tags); // Java 9+,返回不可变集合
}
public String getName() { return name; }
public List<String> getTags() { return tags; } // 已是不可变,无需再包装
}
6.2 封装与单元测试
封装太好可能不利于测试。解决方案:
public class Calculator {
private int internalState;
// 提供 package-private 的测试钩子(仅测试代码可访问)
int getInternalStateForTest() {
return internalState;
}
}
// 测试代码(同包)可直接调用
@Test
void testInternalState() {
assertEquals(0, calculator.getInternalStateForTest());
}
6.3 封装与常见框架
| 框架 | 如何“破坏”封装 | 是否安全 |
|---|---|---|
| Spring | 反射调用 private 构造器/字段 | 框架级后门,业务代码不应模仿 |
| Lombok | 编译时生成 getter/setter | 不破坏,只是减少样板代码 |
| Jackson | 反射读取 private 字段进行序列化 | 可接受,但可用 @JsonIgnore 控制 |
6.4 Java 9+ 模块化封装
// module-info.java
module com.example.banking {
exports com.example.banking.api; // 对外公开
exports com.example.banking.internal to com.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)可以公开,因为它是不可变的:
public static final double PI = 3.1415926; // 公开没问题
八、面试高频题(背下来)
Q1:封装是什么?为什么要封装?
答:把数据和操作捆绑,隐藏内部细节。目的:①保护数据完整性 ②隔离变化 ③控制权限 ④降低耦合。
Q2:有 getter/setter 就算封装吗?
答:不算。那是“贫血模型”。真正封装应该提供业务方法,比如 withdraw() 而不是 setBalance()。
Q3:反射能破坏封装吗?
答:能。setAccessible(true) 可以绕过 private。但这是框架层面的后门,业务代码不应使用。封装防的是粗心,不是恶意。
Q4:封装影响性能吗?
答:现代 JVM 的 JIT 编译器会对 getter/setter 进行内联优化,最终性能与直接访问字段几乎无差别。不要为了微小的性能牺牲设计。
Q5:什么是防御性拷贝?
答:返回内部可变对象(Date、List)时,返回副本或只读视图,防止外部修改内部状态。
Q6:Collections.unmodifiableList 就绝对安全了吗?
答:不是。它只防止增删改结构,但如果列表中的元素本身是可变的,外部仍可修改元素内容。需要元素也是不可变对象,或返回深拷贝。
九、思考题(含深度解析)
题1:下面代码有什么问题?
public class Order {
public List<Item> items = new ArrayList<>();
public void addItem(Item item) { items.add(item); }
}
答案:
- 属性是
public,外部可以直接order.items.clear()破坏数据 - 应改为
private,并提供getItems()返回Collections.unmodifiableList(items) - 注意:
unmodifiableList只防结构修改,如果Item可变,外部仍能order.getItems().get(0).setPrice(0)。如需完全保护,Item也应为不可变类
题2:设计 Temperature 类
要求:存储摄氏度,可获取华氏度,不能低于绝对零度(-273.15°C)。
参考答案:
public class Temperature {
private double celsius;
private static final double ABSOLUTE_ZERO = -273.15;
public Temperature(double celsius) { setCelsius(celsius); }
public double getCelsius() { return celsius; }
public double getFahrenheit() { return celsius * 9 / 5 + 32; }
public void setCelsius(double c) {
if (c < ABSOLUTE_ZERO) throw new IllegalArgumentException("低于绝对零度");
this.celsius = c;
}
public void setFahrenheit(double f) {
setCelsius((f - 32) * 5 / 9); // 复用校验逻辑
}
}
题3(进阶):如何让一个类不能被继承且内部状态完全不可变?
答案:
public final class ImmutableConfig {
private final Map<String, String> settings;
public ImmutableConfig(Map<String, String> settings) {
this.settings = Map.copyOf(settings); // 防御 + 不可变
}
public Map<String, String> getSettings() {
return settings; // 已经是不可变Map,直接返回安全
}
}
关键点:final class + final 字段 + 构造器防御性拷贝 + 不提供修改方法 + 返回不可变视图。
十、总结
| 境界 | 特征 |
|---|---|
| 青铜 | 会用 private + get/set |
| 白银 | 会在 setter 里写校验 |
| 黄金 | 提供业务方法,不暴露内部数据 |
| 铂金 | 会用不可变对象、防御性拷贝、包级私有 |
| 钻石 | 理解模块化封装,能在封装与测试之间找到平衡 |
记住:好的封装,是让别人用你的类时,想出错都难。
更多推荐




所有评论(0)