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()拼接firstName和lastName) - 敏感字段:如密码、令牌,连 getter 都不该有,应通过
matchesPassword(String)这类行为方法验证 - 集合字段:避免返回
list或map的原始引用,应返回Collections.unmodifiableList(...)或新副本
防御性拷贝:防止内部状态被意外修改
当字段是可变对象(Date、ArrayList、自定义可变类)时,getter 必须返回副本,否则调用方修改它会影响对象自身状态。
立即学习“Java免费学习笔记(深入)”;
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)是不可变的,无需拷贝;但老式Date、Calendar必须拷贝 - 数组字段同理:
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类在模块内仍是公开的
封装真正的难点不在语法,而在判断哪些状态该隐藏、哪些行为该暴露、哪些副本必须做——这些都依赖对业务语义的理解,而不是套用模板。










