从源码角度分析hashCode和equals, 再也不背hashCode和equals的覆写规则了:)

原创文章, 转载请私信. 订阅号 tastejava 学习加思考, 仔细品味java之美

什么是hashCode和equals

hashCode和equals都是Object对象中的方法, 也就Java中是所有对象都默认拥有这两个方法. 方法的作用正如其名, hashCode用于返回当前对象的hash值, equals方法用于比较两个对象是否相等.

hashCode和equals默认实现

Object类中hashCode和equals的源代码分别如下所示:

/**
 1. As much as is reasonably practical, the hashCode method defined by
 2. class {@code Object} does return distinct integers for distinct
 3. objects. (This is typically implemented by converting the internal
 4. address of the object into an integer, but this implementation
 5. technique is not required by the
 6. Java™ programming language.)
 7. 大致意思是不同的对象调用此方法返回一个不同的整数, 这个整数通常与对象内存地址有关系.
*/
public native int hashCode();

可以看到hashCode的默认实现是一个本地方法, 虽然看不到具体实现逻辑, 但是可以通过方法注释了解到, 默认的hashCode方法返回值与对象内存地址有关, 即hashCode返回值用内存地址作为逻辑依据.

public boolean equals(Object obj) {
    return (this == obj);
}

equals默认逻辑比较简单, 两个对象内存地址相同返回true, 否则返回false, 即equals默认逻辑也是用内存地址作为逻辑依据的.

为什么需要覆写这两个方法

上面看到两个方法默认逻辑都是与对象内存地址有关, 那么为什么自定义类需要覆写两个方法呢. 其实这是两个问题

  1. 为什么对象需要用equals比较相等而不能直接用=操作符比较两个对象相等.
  2. 为什么自定义对象可能进行hash操作(比如当做HashMap的key, 比如要存入HashSet)时要覆写两个方法.

第一个问题我们都已经很熟悉了, 假设我们有一个自定义类HashKey, 类中包含idNum和name两个字段.

public class HashKey {
    /**
     * 姓名
     */
    private String name;

    /**
     * id
     */
    private String idNum;
    
    // 此处忽略了两个参数的构造器
}

我们实例化了两个HashKey的对象, hashKeyOne和hashKeyTwo, 这两个对象name和idNum字段值相同, 也就是在业务上两个对象相等, 但是此时用equals方法和==操作符比较两个对象结果都是false. 因为两个对象内存地址不同, ==操作符和equals默认逻辑自然返回false.

HashKey hashKeyOne = new HashKey("小明", "220");
HashKey hashKeyTwo = new HashKey("小明", "220");
// 默认equals逻辑下, res1值为false
boolean res1 = hashKeyOne.equals(hashKeyTwo);
// 两个对象内存地址不同, res2值也为false
boolean res2 = hashKeyOne == hashKeyTwo;

所以我们通常覆写equals方法, 如果类中各字段值相同, 那么业务上两个对象就相等. (也有可能是其他业务逻辑, 比如只要id字段相等那么两个对象就想=相等) .
**即用equals比较两个对象相等是为了比较两个对象业务上是否相等.**这就是著名的对象一定要用equals比较相等, 例如基本类型的包装类, JDK已经覆写了equals可以比较业务上相等(数字类型业务相等就是表示的数字值相等).
那么为什么还要覆写hashCode方法呢, 比较两个对象业务相等时我们也没有用到hashCode方法啊. 这要看我们自定义类的实例是否会遇到hash相关操作, 比如自定义对象当做HashMap的key时. HashMap部分源码如下:

// HashMap put方法中关键逻辑.
// 判断当前新增key是否与原位置本身存在的key对象相同, 
// 如果只是hash碰撞导致hash值一样, 但是equals判断业务上不是相同对象, 
// 那么就在此处形成链表, 保存新增key和value. 
// 如果的确是一样的key对象, 那么用新的value替换掉之前存在key对应的value
if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

我们都知道HashMap或者其他Hash容器都是通过新增对象hashCode值来决定对象存放位置.(当然也通过hashCode来达到高效查找. 不需要遍历所有元素直接通过hashCode算出来元素放在哪了)
从源代码逻辑可以看到, 重写hashCode方法主要是为了可能遇到的Hash操作做准备的, 前面重写equals方法已经实现了判断对象业务上相等. 但是HashMap中判断key相等的前提是hashCode值相等, 即有两个对象通过hashCode算出的存储位置相同, 然后才会调用equals进一步确定两个对象相不相等, 不相等用链表存储, 相等直接替换到最新值.
所以重写了equals后, 自定义类的多个实例已经能通过equals方法判断是否业务上相等, 但是在实例可能遇到hash操作时, 还需要重写hashCode方法.

覆写的规则和原理

很多资料告诉我们要背下来hashCode和equals的特性并正确覆写. 这是不对的. 从HashMap源码中可以看到为什么两个不同的独立方法需要有一些规则.

  1. 不同对象hashCode可能相同, equals不一定相同
  2. 相同对象hashCode和equals一定都相同
    第一条规则其实是hash函数的特性, 覆写hashCode方法要尽可能的保证不同对象产生不同hash值, 也就是hash函数结果分布均匀, 这样才是一个好的hash函数实现. 但是再好的函数也会有hash碰撞的时候, 也就是不同对象产生了相同hash值
    第二条规则是因为hash容器判断hashCode和equals都相等, 才是相等对象, 为了正确使用hash相关容器, 需要覆写两个方法并满足第二条规则

怎么写出一个优秀的hashCode方法

一个优秀的hashCode方法要保证hash结果分布均匀, 避免HashMap等hash容器产生链表, 降低效率. 但是想要实现一个优秀的hash算法并不简单, 这里我推荐把自定义类业务上相等涉及的字段生成一个字符串, 然后返回字符串的hashCode结果. JDK已经把String中的hashCode方法做到了很优秀.

是否应该所有自定义类都覆写两个方法

上面我们已经说过了为什么要覆盖这两个方法, 但是往往从事开发有一段时间的人也会有疑惑, 平时没有重写过自定义类这两个方法, 代码也没有出错呀. 那是因为我们没有用到自定义对象当做HashMap的key, 或者没有把自定义对象存入HashSet的逻辑. 当我们用String当做HashMap的key时是逻辑正确的, 因为String内部已经重写了hashCode和equals两个方法.
那么是否所有自定义类都应该覆写两个方法呢, 从防御性编程角度来看, 是的. 这一点我之前有一个用友出身的同事做的特别好, 每个他写的自定义类都会重写equals和hashCode, toString方法.

最佳实践

每个自定义类都覆写equals, hashCode和toString等方法, 简单对象中还要生成每个字段的getter个setter. 阿里Java开发手册提倡把这些业务意义很低的方法放到类尾部, 比private方法还要靠后. 然而这样的策略依旧造成了源代码的膨胀, 并且不能保证每个团队成员遵守规则.
仔细感受, 这些覆写方法都是重复的逻辑, 有没有更简单的实现方式呢? 我目前的最佳实践是使用Lombok, @Data注解自动生成getter, setter, 自动重写hashCode和equals等方法. 如果只需要覆写hashCode和equals, 那么我们可以用lombok的@EqualsAndHashCode注解. 不了解Lombok的同学自行了解一下吧.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值