封装(Encapsulation)
封装是面向对象设计的四大基本原则之一。它的做法是把作用于同一份数据上的数据(变量)和行为(方法)归拢到一个单元(通常是一个类)中 ,并限制对类内部细节的直接访问。
简而言之:
封装 = 数据隐藏 + 受控访问
真实世界类比
把银行账户想象成银行里的一个保险库。你不会自己走进保险库去改动里面的数字。
相反,你是通过一个定义良好的接口------也就是 ATM 来与保险库交互的。
ATM 只提供有限但明确的操作:
deposit()(存款)withdraw()(取款)checkBalance()(查询余额)

你无法直接访问或修改银行内部的账目数据。
银行也许会更换信息存储方式、调整计息规则或交易校验逻辑,但这些变化都不会影响你使用 ATM 的方式。
这就是封装在起作用:隐藏内部复杂度,只暴露必要的内容。
在一个封装良好的设计中,外部代码不需要知道某件事是怎么 完成的,只需要知道能做什么。
1. 为什么封装很重要
封装不只是为了保护数据,更是为了设计出健壮 、安全 且易于维护的系统。
以下是它在实践中重要的原因:
1.1 数据隐藏
敏感数据(如银行余额或密码)不应被直接暴露。封装让这些数据保持私有,只能通过受控的方法来访问。
1.2 受控访问与校验
它确保数据只能以受控的、可预期的方式被修改。
例如,你可以在方法内部对输入做校验,从而阻止无效的存款或取款操作。
1.3 可维护性更强
由于内部细节被隐藏,你可以改变实现方式(例如数据存储或校验的具体做法),而不会影响依赖它的代码。
1.4 安全与稳定
通过阻止外部篡改,封装降低了系统进入不一致或非法状态的风险。
2. 如何实现封装
封装主要通过两种语言特性来实现:访问修饰符 (用于控制可见性)和 getter/setter(用于为私有数据提供受控访问)。
2.1 访问修饰符
访问修饰符是一些关键字,用于控制代码的哪些部分能够看到并与类的字段和方法交互。最常见的三种是:
private:仅能在同一个类内部访问。这是隐藏数据的主要工具。protected:能在同一个类及其子类中访问。当子类需要访问父类的数据时很有用。public:可从任何地方访问。用于定义受控的对外接口。
一条简单的通用规则是:默认把所有东西都设为 private,然后再有选择地把需要公开的内容暴露出来。
下面这个最小示例展示了它们之间的区别:
java
public class Product {
private String name; // Only this class can access
private double price; // Only this class can access
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { // Anyone can read the name
return name;
}
public double getPrice() { // Anyone can read the price
return price;
}
}
2.2 Getter 与 Setter
这些是公共方法,用于对 private 属性提供受控的、间接的访问。
- Getter(例如
getBalance()):提供对某个属性的只读访问。 - Setter(例如
setAmount()):允许修改某个属性,通常内置了校验逻辑。
下面的示例展示了一个 setter 如何阻止非法数据进入对象:
java
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
setPrice(price); // Use the setter for validation
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
this.price = price;
}
}
3. 实战:BankAccount
现在来看一个完整的封装类,它带有恰当的校验、受控访问和业务规则。BankAccount 类把 balance 设为私有,只允许通过 deposit() 和 withdraw() 来修改,而这两个方法各自执行自己的规则。
注意这里发生了什么:
balance被标记为private,因此任何外部类都无法直接访问或修改它。deposit()和withdraw()是公共入口点,它们在更新状态之前会先校验用户输入。getBalance()提供只读访问,既不暴露底层变量,也不允许外部代码改动它。
这样一来,账户始终处于合法状态,业务规则也通过受控接口得到了强制执行。
java
public class BankAccount {
private String accountHolder;
private double balance;
public BankAccount(String accountHolder) {
this.accountHolder = accountHolder;
this.balance = 0.0;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds");
}
balance -= amount;
}
public double getBalance() {
return balance;
}
public String getAccountHolder() {
return accountHolder;
}
}
4. 实际案例:PaymentProcessor
我们来看一个更贴近现实的例子。你正在构建一个 PaymentProcessor 类来处理信用卡交易。原始卡号在任何地方都不允许被存储或可见。如果开发者不小心把支付对象打印到日志里,或者在调试器中查看它,他们应该只能看到一个被掩码处理过的版本。
掩码逻辑、金额以及处理流程,都是调用方无需关心的内部细节。
为什么这个设计有效
- 原始卡号从不被存储。 构造函数在接收到卡号后会立刻对它做掩码处理,因此即便有人通过调试或反射访问了对象的内部状态,他们看到的也只能是掩码后的版本。
- 掩码由私有方法在内部完成。 调用方不需要知道掩码是如何工作的。他们传入完整的卡号,剩下的交给类来处理。
- 外部调用方拿到的是极简接口。 只需创建一个
PaymentProcessor并调用processPayment()即可。既不必先去调用maskCardNumber(),也无需操心如何安全地保存原始卡号。 - 对掩码逻辑的改动被隔离在内部。 如果你之后决定改用不同的掩码方式(比如显示前 4 位而不是后 4 位,或采用其他格式),你只需修改一个私有方法,任何外部代码都不会受影响。
这就是把封装应用到安全场景:敏感数据进入类之后,被转换成安全的表示形式,而原始数据永远不会被暴露。
java
class PaymentProcessor {
private String cardNumber;
private double amount;
public PaymentProcessor(String cardNumber, double amount) {
this.cardNumber = maskCardNumber(cardNumber);
this.amount = amount;
}
private String maskCardNumber(String cardNumber) {
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
}
public void processPayment() {
System.out.println("Processing payment of $" + amount + " for card " + cardNumber);
}
}
public class Main {
public static void main(String[] args) {
PaymentProcessor payment = new PaymentProcessor("1234567812345678", 250.00);
payment.processPayment();
}
}