封装(Encapsulation)

封装(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();
    }
}
相关推荐
yaoxin5211231 小时前
502. Java 反射 - 编写 MessageInterceptor 类
java·开发语言
风流 少年1 小时前
Spring AI 2.0:阿里云百炼平台(工作流应用)
java·后端·spring
不灭的程序员阿澄1 小时前
把多个 AI 网站装进一个常驻托盘的桌面窗口里,像切换原生 App 一样切换 AI
人工智能·chatgpt
长谷深风1111 小时前
AI Tool 设计:粒度、参数与错误恢复怎么做
java·大数据·人工智能·ai agent·agent工作流·智能体设计·ai产品设计
张忠琳1 小时前
【deepseek-harness】DSH 文档合辑 · 篇一:核心架构与概览
ai·agent·deepseek·harness
张彦峰ZYF1 小时前
从“打分模型”到“审计智能体”:Agent-as-a-Judge如何重构复杂AI系统的评测范式
人工智能·agent-as-judge·llm-as-a-judge·agentevaluation·deepswe·verifier·agentic search
peijiping1 小时前
AI多智能体解惑:父子子智能体 vs 团队智能体,为什么主流IDE默认只用前者?
人工智能·ai agent·claude code
aiqianji2 小时前
教AI短篇小说写作的软件操作简单,该怎么挑选呢?
人工智能·python
Yoyo Chen1802 小时前
DeepSeek发布多模态模型deepseek-v4-flash-vision-exp:视觉能力正式接入Agent工作流
人工智能·深度学习·microsoft