Skip to content

封装最佳实践

封装是个技术活,但知道原理不代表做得好。

这一节不聊「应该怎么做」,而是聊聊容易踩的坑怎么避坑

防御性复制:你以为的不可变,其实还是可变的

看这段代码:

java
public class Company {
    private List<String> employees = new ArrayList<>();

    public List<String> getEmployees() {
        return employees; // ❌ 直接返回内部引用
    }
}

调用方拿到引用后,可以直接往里加、删、Clear:

java
Company company = new Company();
company.getEmployees().add("赵六"); // 改动了内部状态!
company.getEmployees().clear();     // 清空了!

这不是开玩笑,这是真实的 bug 来源。如果不想被外界改,就把引用锁死。

两种做法:

java
public class DefensiveCopyDemo {

    static class Company {
        private String name;
        private List<String> employees;

        public Company(String name, List<String> employees) {
            this.name = name;
            // 构造时复制:外部的改动进不来
            this.employees = new ArrayList<>(employees);
        }

        // getter 返回副本:外部拿到也改不动
        public List<String> getEmployees() {
            return new ArrayList<>(employees);
        }

        public void addEmployee(String employee) {
            employees.add(employee);
        }
    }

    public static void main(String[] args) {
        List<String> original = new ArrayList<>();
        original.add("张三");

        Company company = new Company("ABC", original);

        // 修改原列表不影响内部
        original.add("李四");
        System.out.println(company.getEmployees()); // [张三]

        // 通过 getter 获取后修改也不影响
        List<String> emps = company.getEmployees();
        emps.add("王五");
        System.out.println(company.getEmployees()); // [张三]
    }
}

关键就两个原则:

  • 构造时复制:把参数复制一份再存
  • getter 返回副本:永远不把内部集合的引用暴露出去

代价是每次返回都创建一个新对象。对于普通集合,这不值一提;对于超大集合,可以考虑只读包装:

java
public List<String> getEmployees() {
    return Collections.unmodifiableList(employees);
}

但这个只防外层修改,如果集合里装的是可变对象,元素本身还是能被改。更严格的做法是深拷贝,但实际项目中这种情况很少。

不可变对象:最干净的封装

如果你想让一个类完全不可变,最理想的状态是这样:

java
public class ImmutablePoint {

    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }

    // 没有 setter,没有能修改字段的方法
}

不可变对象的优势是全局的:不可变的东西不需要同步,不需要防御性复制,不需要担心线程安全

实际项目里,优先把类设计成不可变的。如果必须可变,就把可变范围控制到最小。

Builder 模式:复杂对象的好帮手

当一个类有很多字段,尤其是可选字段多的时候,重载构造函数很快就失控:

java
// 这是构造函数地狱的开始
User user = new User("张三", 25, null, null, true, null);

Builder 模式把构造过程变成链式调用:

java
public class BuilderPatternDemo {

    static class User {
        private final String name;  // 必填
        private final int age;       // 选填,有默认值
        private final String email;   // 选填
        private final boolean active; // 选填

        private User(Builder builder) {
            this.name = builder.name;
            this.age = builder.age;
            this.email = builder.email;
            this.active = builder.active;
        }

        public static class Builder {
            private String name;
            private int age = 0;
            private String email;
            private boolean active = true;

            public Builder name(String name) {
                this.name = name;
                return this;
            }

            public Builder age(int age) {
                this.age = age;
                return this;
            }

            public Builder email(String email) {
                this.email = email;
                return this;
            }

            public Builder active(boolean active) {
                this.active = active;
                return this;
            }

            public User build() {
                if (name == null || name.isBlank()) {
                    throw new IllegalArgumentException("name 不能为空");
                }
                return new User(this);
            }
        }
    }

    public static void main(String[] args) {
        User user = new User.Builder()
                .name("张三")
                .age(25)
                .email("zhangsan@example.com")
                .build();

        System.out.println(user.name + ", " + user.age);
    }
}

Builder 的好处不只是「链式调用好看」,更关键的是build() 方法里做统一的参数校验。构造函数里的校验容易被忽略,Builder 的 build() 是最后一道关卡,校验逻辑集中、可读。

字段私有后,别用 public 的 getter 暴露一切

很多人在把字段设成 private 后,觉得反正有 getter 了,就随便暴露:

java
public class Account {
    private double balance;

    public double getBalance() {
        return balance; // ❌ 余额直接暴露
    }
}

调用方拿到余额后,你管不住他拿去干什么。加减乘除、业务校验,全在外部分散着做了。

更好的做法是提供行为方法,而不是暴露数据

java
public class Account {
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("存款金额必须大于 0");
        }
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0) {
            return false;
        }
        if (amount > balance) {
            return false;
        }
        balance -= amount;
        return true;
    }
}

这样,余额只能通过特定的操作来改变,而且操作里可以内嵌业务规则。封装的本质是把数据和操作绑在一起,不是把数据藏起来然后到处传。

总结

封装的常见误区:

  • ❌ 字段 private + getter 就完事了——外部拿到的引用可能还是能改
  • ❌ 所有字段都提供 setter——这是在公开内部状态
  • ❌ 构造函数不校验——垃圾进,垃圾出

封装的正确姿势:

  • ✅ 返回副本或只读包装,防止外部修改内部状态
  • ✅ 优先设计不可变类,减少同步和防御成本
  • ✅ 复杂对象用 Builder,校验逻辑集中在 build() 里
  • ✅ 提供行为方法而非裸 getter,让操作和业务规则绑在一起

基于 VitePress 构建