Java中equals和hashCode方法的底层工作原理解析
疑惑的本质是混淆了Java实例方法的静态绑定与动态分派规则,默认Objects.equals中调用的a.equals(b)永远执行Object类的原生实现,这是错误的认知。
涉及的核心代码
自定义Ball类实现:
static class Ball { String color; public Ball(String color) { this.color = color; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ball ball = (Ball) o; return Objects.equals(color, ball.color); } @Override public int hashCode() { return Objects.hash(color); } }
JDK提供的Objects.equals工具方法源码:
public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); }
关键机制
Java中所有非私有、非静态、非final的实例方法都遵循运行时动态分派规则:方法调用的实际实现,不取决于引用变量的声明类型,而取决于引用指向的实际对象的运行时类型。如果该类型重写了对应方法,就优先调用重写后的版本;只有当类型没有重写方法时,才会沿继承链向上调用父类(最终到Object类)的实现。
代码逻辑逐段拆解
Ball类equals方法最后一行调用Objects.equals(color, ball.color)时,传入的两个参数都是String类型实例。String类已经重写了自身的equals方法,逻辑为逐字符比较字符串内容,不会使用Object类原生的"仅判断内存地址是否相同"的逻辑。
Objects.equals方法的完整执行流程:
- 首先判断两个入参引用是否指向同一个对象,是则直接返回true,对应源码中的
a == b判断 - 如果不是同一对象,先校验第一个参数a(即当前Ball实例的color属性)是否为null,避免空指针:如果a为null,直接返回false(第一步已经排除a和b为同一引用的可能,a为null时二者必然不等)
- 如果a不为null,调用a的实际运行时类型重写后的equals方法与b做比较:对String类型而言,就是逐字符比对字符序列,内容完全一致则返回true,否则返回false
完整运行链路示例
创建两个实例Ball b1 = new Ball("red")、Ball b2 = new Ball("red"),调用b1.equals(b2)的执行过程:
- 判断
this == o:b1和b2是堆中两个独立对象,内存地址不同,返回false,进入下一段判断 - 判断o是否为null、运行时类型是否与当前类一致:b2不为null,类型为Ball,判断通过,将o强转为Ball类型
- 调用
Objects.equals(color, ball.color)比较两个实例的color属性:- 如果两个color都指向字符串常量池中的同一个"red",第一步
a==b直接返回true - 如果其中一个color是
new String("red")创建的独立字符串对象,会调用String重写的equals逐字符比较内容,二者字符序列完全一致,同样返回true
- 如果两个color都指向字符串常量池中的同一个"red",第一步
- 最终两个Ball实例的equals判断返回true,符合预期。
补充:
Objects.equals的核心作用是省略手动编写空判断的冗余代码,逻辑和手写判空后调用对象equals的写法完全等价。如果Ball类的属性是自定义引用类型,只要该类型正确重写了equals方法,这里就会自动调用对应类型重写后的逻辑;只有当属性类型没有重写equals时,才会走到Object类的原生equals实现,按内存地址判断相等性。
内容的提问来源于stack exchange,提问作者iank

