做业务迭代开发,很多人都会用继承复用实体类、DTO、参数模型。父类定义公共字段,子类扩展业务字段,日常写起来很省事,也能统一项目字段结构。
但长期迭代下来,团队多人维护的情况下,很容易出现一种非常隐蔽的编码问题:子类重复定义父类已存在的字段。
新手基本不知道 Java 字段隐藏的特性,只觉得名字一样无所谓,编译不报错、启动不报错,测试简单场景也没问题。直到上线后偶尔出现字段赋值为空、数据覆盖不掉、更新无效的问题,排查起来完全摸不着头脑。
我之前在老项目迭代就碰到过这个问题,同样的字段,set赋值成功,get拿出来永远是null,代码逻辑没有任何报错,断点调试看着赋值正常,出方法就失效。
最后一层层排查才发现,是子类和父类存在同名成员变量,出现了字段隐藏。
Java 的方法重写是覆盖,但字段不存在重写,只会隐藏。父类、子类拥有两个完全独立的同名字段,内存中是两份数据,互不干扰。
这就造成了非常诡异的现象:代码里看似在操作同一个字段,实际根据引用类型不同,操作的是完全不同的内存变量。
最常见的场景就是入参接收、对象拷贝、框架赋值。
比如前端传参过来,用子类接收参数,框架反射赋值给子类的同名字段。业务代码里用父类引用接收对象,读取字段时读的是父类的空字段,永远拿不到子类已经赋值的数据。
全程无异常、无日志提示,就是数据对不上。
还有对象拷贝、工具类赋值的场景,问题会更隐蔽。
很多人用工具类做对象属性拷贝,依赖反射遍历字段赋值。遇到父子类同名字段,不同工具的遍历顺序、赋值规则不一样,有的拷贝成功、有的直接跳过。
导致本地测试环境正常,线上偶尔拷贝失效,字段数据丢失,完全没有规律。团队迭代代码越多,继承层级越深,这种问题出现的概率越高。
更坑的是日志打印误导判断。
很多实体重写了 toString 方法,只打印当前类字段,或者只打印父类字段。调试时打印对象,看到字段有值,实际运行逻辑读取的是另一个隐藏字段,数值为空。
肉眼看着数据正常,代码执行结果异常,极大拉长排查时间。
这类问题基本都是团队协作不规范导致的。
多人迭代项目,没人梳理继承关系,新人改代码不知道父类已有字段,随手在子类重新定义一遍。注解、默认值、字段类型还可能和父类不一样,进一步加剧数据错乱。
有的子类字段加了注解,父类同名字段没有注解,或者反之。序列化、数据库映射、参数校验只生效其中一个,导致对接、入库、解析各种隐性bug。
长期维护老项目的人应该都有体会,继承滥用真的很容易堆积隐患。
字段隐藏这种问题,不属于代码错误,属于语法特性带来的逻辑歧义。编译器不会提醒,IDE 大部分时候也不会强警告,全靠开发自己规避。
现在我写代码,除非是非常稳定的公共基础字段,否则尽量少用实体继承。就算用继承,也会严格核对父子类字段,杜绝同名重复定义。
很多看似无伤大雅的代码习惯,日积月累,就是线上最难排查的偶发故障。
Java开发复盘:继承字段隐藏引发的隐形数据赋值异常