必须协同重写tostring、equals与hashcode:tostring需简洁安全可读,equals与hashcode必须逻辑一致且同时重写,否则hashmap/hashset等集合会出现存取失败、重复添加等问题。

Java 开发中,Object 类的 toString、equals、hashCode 三个方法必须协同重写,不能只改一个或两个。它们不是独立功能,而是一套语义契约——尤其 equals 和 hashCode 必须严格保持逻辑一致,否则集合类(如 HashMap、HashSet)行为会出错:对象存不进、取不出、重复添加、删不掉。
toString:清晰、安全、可读
默认返回 类名@哈希码(如 User@1b6d3586),对调试、日志、监控毫无价值。重写目标是:一眼看清关键状态,且不抛异常、不泄露敏感信息、不触发副作用。
- 只包含核心业务字段,比如
id、name、status;排除密码、token、大文本、计算字段 - 格式统一推荐:
ClassName{field1=value1, field2=value2},便于人眼扫描和日志解析 - null 值显式表达,用
Objects.toString(field, "<null>")</null>或String.valueOf(field),避免拼接时 NPE - 不调用可能阻塞或抛异常的方法(如远程接口、数据库查询、复杂 getter)
- 不递归调用关联对象的 toString(防止栈溢出),双向引用时需手动断链
equals:满足五项数学契约
重写 equals 不是“让两个对象看起来一样”,而是定义“在当前业务语境下,什么才算逻辑相等”。它必须同时满足自反性、对称性、传递性、一致性,并正确处理 null。
- 开头先用
this == obj判断引用相等,提升性能 - 类型检查用
getClass() == obj.getClass(),不用instanceof—— 否则在继承场景下易破坏对称性(父类.equals(子类) 为 true,但子类.equals(父类) 因字段多而 false) - 字段比较全部使用
Objects.equals(a, b),自动处理 null,避免手写a != null && a.equals(b)漏判 - 只比较影响业务相等性的字段,忽略数据库 ID(除非主键即业务标识)、时间戳、版本号、缓存字段
- 不修改对象状态,不抛受检异常,不依赖外部资源
hashCode:与 equals 字段完全对齐
只要 a.equals(b) == true,就要求 a.hashCode() == b.hashCode()。这是哈希集合能正常工作的底层前提。反过来不成立——哈希码相同,不一定相等(允许哈希冲突)。
- 必须基于 和 equals 中完全相同的字段 计算,顺序也建议一致(虽不强制,但利于维护)
- 优先使用
Objects.hash(f1, f2, f3),它内部已做 null 安全处理,比手写31 * f1.hashCode() + f2.hashCode()更可靠 - 绝对避免使用可变字段(如未 final 且后续可能 set 的属性),否则对象加入 HashSet 后改了字段,哈希桶位置就失效,再也找不到了
- 如果类可被继承,且子类要扩展 equals 逻辑,父类的 equals 和 hashCode 应声明为
final,或明确约定子类必须重写并调用 super
工具与验证:别靠手写,要靠测试
IDE 自动生成(IntelliJ/Eclipse)可起步,但必须人工核对字段是否完整、类型是否匹配、继承处理是否合理。Lombok 的 @ToString、@EqualsAndHashCode 能大幅减少模板代码,但务必显式指定 include 或 exclude 字段,禁用默认全量包含。
- 单元测试至少覆盖:自反性(x.equals(x))、对称性(x/y 互调)、传递性(x=y && y=z ⇒ x=z)、一致性(字段不变时结果稳定)、null 安全
- 集合验证:把两个逻辑相等的对象放进 HashSet,确认 size=1;放进 HashMap 当 key,确认不会覆盖
- 检查生成的 hashCode 是否遗漏字段,或误将 boolean 字段用
field ? 1 : 0(应使用Boolean.hashCode(field))
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











