封装:你的数据不该被随意触碰

从入门到面试,一篇就够了(真正完整版)

一、引言:一个“太开放”的惨痛教训

想象你开了一家银行,为了“方便客户”,你把金库的钥匙挂在门口,上面贴了张纸条:“请自行取用,用完放回”。

结果可想而知。这很荒唐,对吧?

但在编程世界里,很多初学者就在做同样荒唐的事:

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 在多线程下不安全。实际项目中可用 AtomicLongsynchronized

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); }
}

答案

  1. 属性是 public,外部可以直接 order.items.clear() 破坏数据
  2. 应改为 private,并提供 getItems() 返回 Collections.unmodifiableList(items)
  3. 注意: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 里写校验
黄金 提供业务方法,不暴露内部数据
铂金 会用不可变对象、防御性拷贝、包级私有
钻石 理解模块化封装,能在封装与测试之间找到平衡

记住:好的封装,是让别人用你的类时,想出错都难。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐