树码科技 | 技术博客

Java 中的封装

2025/12/27
58
0

学习目标

  • 理解封装的核心思想:数据与行为的统一 + 信息隐藏

  • 掌握在 Java 中通过 访问修饰符、getter/setter 实现封装

  • 能够设计安全、可维护、高内聚的类结构

  • 了解封装的优势与潜在代价,避免过度或不足封装


什么是封装

封装(Encapsulation) 是面向对象编程(OOP)的四大基本原则之一。它的核心理念是:

“将数据(属性)和操作数据的方法(行为)捆绑在一个单元(类)中,并对外隐藏内部实现细节。”

Encapsulation.webp

类比理解:胶囊

就像药片把有效成分包裹在胶囊壳内:

  • 你不需要知道药物如何合成

  • 你只需按说明服用(调用公共方法)

  • 制造商可以随时改进配方(内部实现),只要服用方式不变


如何实现封装

Java 通过以下机制实现封装:

步骤

说明

声明字段为 private

阻止外部直接访问或修改数据

提供 public 的 getter/setter

控制对数据的读写,可加入验证逻辑

使用合适的访问修饰符

精细控制类成员的可见性

基础示例:程序员类

class Programmer {
    // 1. 数据私有化
    private String name;

    // 2. 提供受控访问
    public String getName() {
        return name;
    }

    public void setName(String name) {
        // 可在此加入验证逻辑(如非空检查)
        if (name != null && !name.trim().isEmpty()) {
            this.name = name.trim();
        } else {
            throw new IllegalArgumentException("姓名不能为空");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Programmer p = new Programmer();
        p.setName("张三");
        System.out.println("姓名 => " + p.getName()); // 输出:姓名 => 张三
    }
}

关键点:

  • 外部无法直接写 p.name = "李四"(编译错误)

  • 所有修改必须通过 setName(),确保数据合法性


封装的进阶应用

数据验证与业务逻辑

封装不仅是“加 getter/setter”,更是保障对象状态一致性的手段。

示例:银行账户类

class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        setBalance(initialBalance); // 使用 setter 初始化
    }

    public double getBalance() {
        return balance;
    }

    // 不提供 setBalance 公开方法!防止随意修改余额
    private void setBalance(double balance) {
        if (balance < 0) {
            throw new IllegalArgumentException("余额不能为负数");
        }
        this.balance = balance;
    }

    // 提供业务方法:存款
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("存款金额必须大于 0");
        }
        this.balance += amount;
    }

    // 提供业务方法:取款
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("取款金额必须大于 0");
        }
        if (amount > balance) {
            throw new IllegalStateException("余额不足");
        }
        this.balance -= amount;
    }

    public String getAccountNumber() {
        return accountNumber; // 账号只读,不提供 setter
    }
}

安全设计:

  • 余额不能被直接设置

  • 所有资金变动必须通过 deposit() / withdraw() 完成

  • 账号一旦创建不可更改(只读)


封装的优势

优势

说明

数据隐藏

防止外部非法访问或篡改内部状态

提升安全性

可在 setter 中加入校验(如邮箱格式、年龄范围)

易于维护

内部实现变更(如存储方式)不影响调用方

增强可读性

类职责清晰,行为集中

保证数据完整性

对象始终处于合法状态(如余额 ≥ 0)

促进代码复用

封装良好的类可作为可靠组件在多项目中使用


封装的潜在缺点

缺点

说明

应对策略

代码量增加

每个字段需写 getter/setter

使用 IDE 自动生成;
或仅对需要控制的字段封装

微小性能开销

方法调用 vs 直接访问

现代 JVM 会内联优化,实际影响极小

过度封装降低灵活性

如完全禁止继承或扩展

合理设计
必要时使用 protected 或提供扩展点

重要提醒:不要为了封装而封装!

如果某个字段天然无需验证(如 id、createdAt),且确定不会变更行为,可考虑简化设计。


封装 vs 其他 OOP 原则

原则

关注点

与封装的关系

抽象

“做什么” vs “怎么做”

封装是实现抽象的手段之一

继承

代码复用

封装保护父类内部细节,子类通过接口交互

多态

同一接口不同实现

封装确保每个实现类内部状态安全


最佳实践

  1. 默认将字段设为 private除非有充分理由(如工具类常量可用 public static final)

  2. getter/setter 不是必须的

    • 只读属性:只提供 getter

    • 内部状态:不提供任何访问器

    • 业务行为:用 deposit() 代替 setBalance()

  3. 在 setter 中加入验证逻辑

    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在 0~150 之间");
        }
        this.age = age;
    }
  4. 避免“贫血模型”不要只写 getter/setter 而无业务方法。好的封装类应包含行为,而不仅是数据容器。


重点总结

  • 封装 = 数据私有化 + 受控访问

  • 核心目的:保护对象内部状态,提升系统健壮性

  • 实现方式:private 字段 + public 方法(含验证)

  • 封装不是目的,而是实现高内聚、低耦合设计的手段

  • 合理封装让代码更安全、更易维护、更易测试


思考题

  1. 如果一个类的所有字段都是 public,它是否违反了封装原则?为什么?

  2. 在什么情况下,你可以不为私有字段提供 setter 方法?请举例说明。

  3. 如何在保持封装的同时,允许子类访问父类的某些内部状态?