不可变对象必须重写equals和hashcode,因object默认实现基于引用地址,会导致hashset重复、hashmap查找失败;tostring重写提升可观测性但需防异常与敏感信息泄露;final类可杜绝多态引发的等价性破坏。

理解 Object 类在不可变对象设计中的作用,关键在于:它提供的三个核心方法——toString、equals 和 hashCode——不是“可有可无的装饰”,而是参与对象语义、集合行为与线程安全的契约接口。若不按不可变性原则重写,会直接破坏封装、引发哈希冲突、导致集合查找失败,甚至让多态等价判断失效。
为什么不可变对象必须重写 equals 和 hashCode
默认的 Object.equals() 比较的是引用地址,而不可变对象的“相等”应基于内容(如两个 String 值相同即相等)。若不重写:
- 两个内容相同的不可变实例在
HashSet中会被视为不同元素,重复插入 -
HashMap中用该对象作 key 时,get()可能返回null,因为hashCode()返回的是内存地址散列值,每次新建对象都不同 - 子类若扩展字段但未调用
super.equals(),会违背等价关系的传递性
正确做法是:仅使用 final 字段 参与比较和哈希计算,并确保两者逻辑一致。例如:
public final class Point {
private final int x, y;
// 构造器略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y); // 与 equals 使用完全相同的字段
}
toString 重写的实用性和安全性边界
重写 toString 不影响功能正确性,但极大影响可观测性与调试安全:
- 避免打印出
Point@1a2b3c这类无意义信息,应输出Point{x=3, y=4} - 不建议在
toString中调用可能抛异常或触发复杂计算的方法(如访问未初始化的懒加载字段),否则日志打印时意外崩溃 - 对含敏感字段(如密码、token)的不可变对象,
toString应做脱敏处理,例如显示Token{value='***'}
final 类 + 正确重写 = 彻底封堵多态漏洞
不可变对象通常声明为 final 类(如 String),这不仅是防止继承篡改,更是保障 equals 和 hashCode 行为稳定的前提:
- 若类可被继承,子类可能添加新字段,但父类的
equals无法感知,导致a.equals(b)为true,而b.equals(a)为false(违反对称性) - 使用
final类后,getClass() != o.getClass()校验即可安全 cast,无需用instanceof留出子类余地 - Lombok 的
@Value注解正是基于此逻辑:自动生成final类 + 不可变字段 + 安全的equals/hashCode/toString
避开 Object 方法重写的常见陷阱
这些错误看似微小,却会在运行时暴露不可变性缺陷:
- 在 equals 中比较了非 final 字段:若字段本应不可变却被意外修改(如反射绕过),等价性瞬间崩塌
- hashCode 使用了缓存但未同步初始化:多线程首次调用可能生成不同哈希值,违反契约
-
重写 toString 时调用了 getter,而 getter 内部做了防御性拷贝(如返回
new ArrayList(tags)):频繁日志会引发不必要的对象分配 -
忽略 null 安全:比如用
name.equals(other.name)而非Objects.equals(name, other.name),一旦字段为 null 就 NPE
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











