1. 为什么说你的类成了"公共厕所"?
上周review新同事的代码时,我看到一个触目惊心的User类——所有字段都是public的,十几个外部类直接修改它的内部状态。这就像把类设计成了公共厕所,谁都可以随意进出。这种写法在Java开发中会引发连锁灾难:数据被意外篡改、业务逻辑散落各处、排查bug如同大海捞针。
封装(Encapsulation)作为面向对象三大特性之首,本质上是通过访问控制建立类与外部世界的防火墙。就像你不会把银行卡密码写在便利贴上,类的内部状态也不应该裸奔。合理的封装能让代码具有自我防御能力,我在金融支付系统开发中就深有体会——所有核心业务类的字段都是private的,通过严格的方法控制访问,五年线上运行零数据异常。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 访问修饰符:Java的四大门神
2.1 四种访问权限详解
Java用四个关键字控制访问边界,我把它们比作不同等级的门禁:
java复制public class SecurityDemo {
public String frontDoor = "任何人都可以进出"; // 小区大门
protected String familyDoor = "家人和亲戚可进"; // 自家防盗门
String neighborDoor = "同楼邻居可进"; // 默认权限-楼道门
private String safeBox = "只有自己能开"; // 保险柜
}
实测案例:在电商订单系统中,订单金额(orderAmount)必须用private保护。我曾见过因为用默认权限导致促销模块直接修改金额,造成百万元资损的惨案。
2.2 封装的最佳实践
- 字段一律private:就像你不会公开日记本,字段是类的隐私
- 通过方法控制访问:getter/setter是经过安检的访客通道
- 保护不可变对象:final字段+private实现终极防御
java复制public class BankAccount {
private final String accountId; // 一旦创建不可修改
private BigDecimal balance;
// 唯一修改余额的受控通道
public void deposit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("存款金额必须大于零");
}
this.balance = this.balance.add(amount);
}
}
3. Getter/Setter:不是简单的数据通道
3.1 为什么需要访问器方法
直接看这段灾难代码:
java复制user.age = -10; // 年龄可以是负数?!
用getter/setter可以:
- 数据校验(年龄不能为负)
- 触发事件(余额变动短信通知)
- 延迟加载(首次访问才查数据库)
3.2 高级封装技巧
- 防御性拷贝:防止外部修改内部引用
java复制private List<Order> orders;
public List<Order> getOrders() {
return new ArrayList<>(orders); // 返回副本
}
- Builder模式:复杂对象的可控构造
java复制User user = new User.Builder()
.name("张三")
.age(30)
.build();
- 不可变对象:终极线程安全方案
java复制public final class ImmutablePoint {
private final int x;
private final int y;
// 只有构造函数能赋值
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
}
4. 封装实战:设计一个安全的银行账户类
4.1 需求分析
- 账户余额不能被随意修改
- 存取款需要记录流水
- 转账需验证双方账户状态
4.2 类设计
java复制public class BankAccount {
private final String accountNumber;
private BigDecimal balance;
private List<Transaction> transactions;
private boolean isActive;
// 构造器也需保护
public BankAccount(String accountNumber) {
this.accountNumber = Objects.requireNonNull(accountNumber);
this.balance = BigDecimal.ZERO;
this.transactions = new ArrayList<>();
this.isActive = true;
}
public void deposit(BigDecimal amount) {
validateAmount(amount);
this.balance = balance.add(amount);
addTransaction("DEPOSIT", amount);
}
public void withdraw(BigDecimal amount) {
validateAmount(amount);
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
this.balance = balance.subtract(amount);
addTransaction("WITHDRAW", amount.negate());
}
private void validateAmount(BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("金额必须大于零");
}
}
// 转账操作原子性控制
public static void transfer(BankAccount from, BankAccount to, BigDecimal amount) {
synchronized (BankAccount.class) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
5. 封装破环者:反射攻击与防御
即使private也挡不住反射攻击:
java复制Field field = obj.getClass().getDeclaredField("secret");
field.setAccessible(true); // 暴力破解
field.set(obj, "hacked");
防御方案:
- 安全管理器
java复制System.setSecurityManager(new SecurityManager());
- 字段加密存储
- 使用模块系统(Java 9+)
6. 封装在框架中的应用
6.1 Spring的依赖注入
看似直接给字段赋值,实则通过setter或构造器:
java复制@Component
public class OrderService {
private final PaymentGateway gateway; // 通过构造器注入
@Autowired
public OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
6.2 JPA实体类封装
即使数据库字段公开,实体类也应封装:
java复制@Entity
public class Product {
@Id
@GeneratedValue
private Long id; // 不直接暴露数据库ID
@Column(nullable = false)
private String name;
// 必须提供protected无参构造器
protected Product() {}
// 业务构造器控制创建逻辑
public Product(String name) {
this.name = validateName(name);
}
}
7. 常见封装误区与纠正
7.1 过度封装反模式
- 无脑生成getter/setter:有些字段根本不需要外部访问
- 多层代理嵌套:getUser().getAccount().getBalance() 违反迪米特法则
- 滥用protected:子类继承不是随意访问的理由
7.2 性能优化误区
"直接访问字段比调用方法快"——现代JVM会内联简单getter/setter,性能差异可以忽略。我在高频交易系统中实测,合理封装的方法调用纳秒级耗时。
8. 封装与设计模式的结合
8.1 工厂模式隐藏构造细节
java复制public class CloudStorage {
private CloudStorage() {} // 禁止直接构造
public static CloudStorage create(String config) {
validateConfig(config);
return new CloudStorage(config);
}
}
8.2 代理模式控制访问
java复制public class SensitiveDataProxy {
private RealData realData;
public String getMaskedInfo() {
if (!checkPermission()) {
throw new SecurityException();
}
return realData.getInfo();
}
}
9. 代码审查中的封装检查清单
- [ ] 所有字段是否都是private?
- [ ] 是否有不需要setter的字段?
- [ ] getter是否返回了可变对象的防御性拷贝?
- [ ] 构造器是否完成了参数校验?
- [ ] 是否用final修饰不应被继承的类?
- [ ] 静态常量是否用public final声明?
10. 从Java看其他语言的封装
10.1 Kotlin的简洁封装
kotlin复制class Person(
val name: String, // 只读属性
private var age: Int // 私有可变
) {
fun celebrateBirthday() {
age++ // 外部无法直接修改
}
}
10.2 Python的约定封装
虽然Python没有真正的private,但约定:
python复制class Account:
def __init__(self):
self.__balance = 0 # 名称修饰为_Account__balance
@property
def balance(self): # 控制访问
return self.__balance
在团队协作中,我看到过最优雅的代码都是封装良好的——每个类都像精心设计的自动售货机,只暴露必要的按钮,内部机制被安全保护。当你养成封装的肌肉记忆后,会发现自己代码的bug率显著下降,这就是面向对象防御性编程的魅力。
