必须重写 tostring()、equals() 和 hashcode() 三者:tostring() 不重写则输出内存地址,影响调试;equals() 与 hashcode() 必须同时重写且逻辑一致,否则集合操作异常;== 与 equals() 默认行为相同,仅重写后才体现差异。

toString() 不重写就只能看到内存地址
调用 toString() 时如果没重写,Java 默认返回 类名@哈希值十六进制(比如 Person@1b6d3586),这根本不是你想要的“张三, 25岁”这种可读内容。
实际开发中,日志打印、调试输出、JSON 序列化前的 toString 调用都依赖这个方法——不重写,排查问题时就得反复点开对象字段,效率极低。
- IDE 自动生成的
toString()(如 IntelliJ 的Generate → toString())基本够用,但注意排除敏感字段(如密码)、循环引用字段(如父子关系) - 用 Lombok 的
@ToString(exclude = "password")更省事,但编译后字节码里仍存在该方法,不影响运行时行为 - 别在
toString()里做耗时操作(如查数据库、格式化大数组),它可能被日志框架频繁触发
equals() 和 hashCode() 必须一起重写
只重写 equals() 不重写 hashCode(),会导致对象放进 HashMap 或 HashSet 后找不到自己——因为哈希桶定位错了,equals() 根本没机会执行。
典型现象:两个逻辑相等的对象(user1.equals(user2) == true),但 set.contains(user2) 返回 false;或作为 HashMap 的 key 时,map.get(user2) 拿不到值。
- 判断依据必须一致:如果
equals()比较了id和name,那hashCode()就得基于这两个字段计算(可用Objects.hash(id, name)) - 不要用可变字段(如
status、lastLoginTime)参与hashCode()计算,否则对象加入集合后改了字段,哈希值就变了,再也找不回来 - Lombok 的
@EqualsAndHashCode默认包含所有非静态非瞬态字段,记得用exclude或of显式控制
== 和 equals() 的区别不是“值 vs 引用”这么简单
== 比较的是引用是否指向同一块内存,而 equals() 默认行为也是一样——所以没重写时两者效果完全相同。真正关键在于:谁重写了 equals(),以及怎么写的。
常见误判场景:String a = "hello"; String b = new String("hello");,此时 a == b 是 false,但 a.equals(b) 是 true,因为 String 类重写了 equals() 做字符序列比对。
- 自定义类不重写
equals(),就别指望它能按业务逻辑比较;哪怕字段值全一样,equals()仍返回false - 注意空指针:直接调用
obj1.equals(obj2),若obj1为null会抛NullPointerException;更安全写法是Objects.equals(obj1, obj2) - 继承场景下,如果父类已重写
equals(),子类想扩展比较逻辑(比如加个version字段),得先调用super.equals(other),再检查子类字段
hashCode() 碰撞不是 bug,但设计不好会影响性能
两个不同对象 hashCode() 相同是合法且常见的(哈希碰撞),JVM 允许;但若大量对象集中在一个哈希桶里,HashMap 会退化成链表甚至红黑树查找,O(1) 变成 O(n)。
比如用单一字段(如 int status,只有 0/1/2 几个值)生成 hashCode(),所有 status=1 的对象全挤进同一个桶,插入和查询明显变慢。
- 优先选不变、分布均匀的字段:主键 ID、UUID、组合多个字段(如
Objects.hash(id, type, createdAt)) - 避免用浮点数字段(
float/double)直接参与哈希计算,精度问题易导致本该相等的对象hashCode()不同 - 测试时可粗略验证:造一批业务典型数据,统计
hashCode() % N(N 取哈希表默认初始容量 16)的分布,看是否严重倾斜
最容易被忽略的是:重写 hashCode() 时忘了同步更新逻辑——比如加了个新业务字段要参与比较,却只改了 equals(),漏了 hashCode()。这种问题上线后难复现,只能靠代码审查或单元测试卡住。










