Java封装靠访问修饰符与设计意识实现,public字段破坏封装导致API不兼容;应优先用private字段,getter返回不可变副本或新列表,record适用于不可变DTO场景,构造器校验失败抛IllegalArgumentException等明确异常。

Java 中的封装不是靠关键字实现的,而是靠访问修饰符 + 设计意识共同完成的;不加 private 修饰的字段、不校验入参的 setter、直接返回可变内部对象的 getter,都会让封装形同虚设。
为什么 public 字段会破坏封装
一旦字段声明为 public,外部代码就能绕过所有逻辑直接读写,后续想加校验、日志、通知或改用计算属性,都会变成不兼容的 API 破坏性变更。
常见错误现象:
- 把
String name声明为public,结果业务中出现空字符串、全空格、超长名等脏数据无人拦截 - 把
List设为- items
public,外部调用items.clear()导致状态意外丢失
正确做法:
立即学习“Java免费学习笔记(深入)”;
- 所有字段优先用
private - 如需读取,提供
public的getter(且返回不可变视图或副本) - 如需修改,提供带校验逻辑的
setter或更明确语义的方法(如addItem(Item item))
getter 返回 List 时如何避免泄露内部引用
直接返回私有 List 字段会导致调用方修改它,进而污染原对象状态 —— 这是封装最常被忽略的破口。
示例问题代码:
private Listtags = new ArrayList<>(); public List getTags() { return tags; // ❌ 危险:返回原始引用 }
修复方式取决于使用场景:
- 只读场景 → 返回不可变副本:
Collections.unmodifiableList(tags) - 允许调用方安全遍历但不修改 → 返回新
ArrayList:new ArrayList(tags) - 字段本身应不可变 → 初始化后不再添加/删除 → 声明为
private final List,并在构造器中赋值
注意:unmodifiableList 只阻止结构修改(add/remove),不阻止元素自身状态变化(如果元素是可变对象)。
何时该用 record 而不是传统 class 封装
record 是 Java 14+ 提供的不可变数据载体,它自动封装了字段、getter、equals、hashCode 和 toString,但**不提供 setter,也不支持继承或自定义字段行为**。
适用场景:
- 纯数据传输对象(DTO)、配置项、函数返回的简单结构体
- 字段全部在构造时确定,之后绝不修改(例如
Point(int x, int y)) - 不需要对单个字段做校验(校验只能放在构造器里,且 record 构造器不能重载)
不适用场景:
- 需要懒加载、缓存计算字段、字段间依赖校验(如 “password” 和 “confirmPassword” 一致性)
- 需要运行时动态更新某个字段(如
status随流程变化) - 要序列化为特定 JSON 格式(record 默认序列化名即字段名,无法用
@JsonProperty等微调)
构造器中校验失败该抛什么异常
封装的边界从对象创建就开始。构造器是第一道防线,校验失败必须中断实例化,而不是静默修正或设默认值。
推荐做法:
- 参数为空或违反业务约束(如负数 ID、null 名称)→ 抛
IllegalArgumentException - 参数格式非法(如传入非 ISO8601 时间字符串)→ 抛
DateTimeParseException或自定义检查异常(如InvalidTimeFormatException) - 避免抛
NullPointerException:它应留给 JVM 自动触发,而非手动 throw;主动校验 null 应用IllegalArgumentException或Objects.requireNonNull
示例:
public User(String name, int age) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("name cannot be null or blank");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("age must be between 0 and 150");
}
this.name = name.trim();
this.age = age;
}
封装真正的难点不在语法,而在判断哪些状态该暴露、以什么粒度和契约暴露;一个 getItems() 方法背后,可能藏着是否可修改、是否线程安全、是否已排序、是否含重复项等隐含假设 —— 这些都得靠命名、文档和类型系统一起守住。










