封装的核心是控制访问而非单纯隐藏字段,如private int age需配合setAge校验年龄范围(0≤age≤150),否则private形同虚设。

封装不是“把属性设成private就完事了”
很多初学者以为只要把字段加个 private,再补上 get/set 方法,就算完成了封装。其实不然——封装的核心是“控制访问”,而不是“隐藏字段”。比如你写了一个 private int age;,但又在 setAge(int age) 里不做校验,外部仍能传入 -5 或 200,那这个 private 就只是个摆设。
真正有效的封装要配合业务逻辑做约束:
-
setAge应该检查范围(如0 ),非法值直接抛IllegalArgumentException - 如果
age一旦设定就不该被修改,那就只提供getAge(),不写setAge() - 如果计算依赖多个字段(如
isAdult()基于age和当前年份),就把逻辑放在类内部,而不是让调用方自己拼条件
什么时候不该暴露setter:从可变性说起
Java 中的封装常被误用为“一律提供 setter”。但现实里大量对象本应不可变(immutable)——比如 Person 的身份证号、订单的创建时间、配置项的键名。一旦允许随意修改,就可能引发状态不一致、线程安全问题或缓存失效。
判断是否该加 setter 的关键不是“将来会不会改”,而是“这个值是否属于该对象的固有、稳定特征”:
立即学习“Java免费学习笔记(深入)”;
- 构造时确定且永不变更的字段 → 只在构造器中赋值,无
setter,字段声明为final - 需要更新但必须受控的字段(如用户邮箱)→ 提供带校验和审计日志的专用方法(如
changeEmail(String newEmail)),而非裸setEmail() - 纯粹的计算结果(如
getFullName())→ 不存为字段,每次调用实时拼接,避免数据滞后
getter/setter不是万能胶水,过度使用反而破坏封装
当你发现一个类里塞了十几个 getXXX()/setXXX(),而且调用方总在反复读-改-写多个字段(比如先 getA(),再 getB(),算出新值后 setC()),说明职责已经外泄。这违反了“信息专家原则”:行为应该靠近它所操作的数据。
更合理的做法是把这类组合操作收归类内:
- 把
calculateDiscount()这类逻辑放进类本身,而不是让外部拿一堆字段去算 - 用 builder 模式替代链式
setX().setY().setZ(),既提升可读性,也便于在构建末尾统一校验 - 对集合字段(如
private List),不要返回原始引用(tags; return tags;),而应返回不可修改视图(Collections.unmodifiableList(tags))或拷贝
继承场景下封装容易被绕过:protected和包级可见的风险
很多人用 protected 字段或方法来“方便子类扩展”,但这等于主动撕开封装边界。子类可以直接修改父类的内部状态,破坏父类自身的不变量(invariant)。例如父类保证 balance >= 0,但子类在重写方法里执行 this.balance = -100;,整个一致性就崩了。
更安全的继承设计习惯:
- 字段一律
private,哪怕要留给子类用,也通过protected final方法暴露只读能力(如protected final BigDecimal getBalance()) - 想让子类定制行为?用模板方法模式:定义
protected void onPaymentProcessed()这样的钩子,而不是开放字段 - 慎用默认(包级)访问权限——它让同一包下所有类都能直击内部,等同于把封装锁换成纱窗
封装最难的部分,往往不是语法怎么写,而是判断哪些细节真该隐藏、哪些契约必须守住。它更多是一种设计取舍,而不是代码格式规范。










