在Java里如何实现对象封装_Java封装原则与实践说明

Java封装靠访问控制符与设计约定实现,private是起点而非终点;需防御性拷贝可变对象、慎用getter/setter、合理使用record等现代特性。

Java 中的对象封装不是靠语法强制,而是靠访问控制符 + 设计约定共同实现的;private 字段配 public 的 getter/setter 只是常见手段,不是全部,更不是目的。

为什么不能直接把字段设为 public

暴露字段会破坏类的不变量(invariant),让外部代码绕过校验逻辑直接写入非法值。比如一个表示年龄的 int age,若设为 public,调用方可以直接赋值 person.age = -5,而本该由 setAge(int) 拦截。

  • private 是封装的起点,不是终点 —— 后续还要考虑 getter/setter 是否真该暴露、是否可变、是否线程安全
  • 即使用了 private,如果返回了内部可变对象(如 ArrayList)的引用,外部仍能修改状态,这叫“浅封装”
  • Lombok 的 @Data 自动生成 getter/setter,但不会自动防御性拷贝,需手动处理

getter/setter 不是万能的:什么时候不该加

不是每个 private 字段都需要一对 getter/setter。暴露接口意味着承担长期兼容责任,一旦发布就很难删除或改签名。

  • 只读字段:用 private final + 单个 getter,不提供 setter(如 id、创建时间)
  • 计算属性:用 getter 封装逻辑,不对应字段(如 getFullName() 拼接 firstNamelastName
  • 敏感字段:如密码、令牌,连 getter 都不该有,应通过 matchesPassword(String) 这类行为方法验证
  • 集合字段:避免返回 listmap 的原始引用,应返回 Collections.unmodifiableList(...) 或新副本

防御性拷贝:防止内部状态被意外修改

当字段是可变对象(DateArrayList、自定义可变类)时,getter 必须返回副本,否则调用方修改它会影响对象自身状态。

public class Person {
    private Date birthDate;

    public Date getBirthDate() {
        return (birthDate != null) ? new Date(birthDate.getTime()) : null; // 防御性拷贝
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = (birthDate != null) ? new 

Date(birthDate.getTime()) : null; } }
  • java.time 类(如 LocalDateTime)是不可变的,无需拷贝;但老式 DateCalendar 必须拷贝
  • 数组字段同理:return Arrays.copyOf(this.data, this.data.length)
  • 自定义类若未声明 final 且含可变字段,也需深拷贝或确保其不可变

现代 Java 封装的补充实践

Java 14+ 的 record 天然支持不可变封装,但仅适用于“纯数据载体”。它默认生成 public 的 accessor 方法,且所有字段隐式 final,无法添加校验逻辑。

public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Coordinates must be non-negative");
        }
    }
}
  • record 的构造器参数校验只能在紧凑构造器中做,不能在普通 setter 里(因为没有 setter)
  • 若需懒加载、缓存、事件通知等行为,record 不适用,仍应回归传统类 + 显式封装
  • 模块系统(module-info.java)可进一步限制包外访问,但需注意:模块级封装 ≠ 类级封装,public 类在模块内仍是公开的

封装真正的难点不在语法,而在判断哪些状态该隐藏、哪些行为该暴露、哪些副本必须做——这些都依赖对业务语义的理解,而不是套用模板。