【小知识探究系列一】为什么Object方法的equals和hashcode方法需要同时被重写

之前看java技术相关书时总会看到这样一句话,即如果在自定义类中重写equals方法,那么同时需要重写hashcode方法。但是对深层原因没有去深究,本片文章就去一探究竟,为什么equals方法和hashcode方法需要同时被重写。
在这里插入图片描述

结论:同时重写equals和hashcode方法的目的是方便我们将当前类的对象准确的插入到散列表中。原因如下:

要想探究这个问题的原因,首先就得从往散列表插入一条数据的实现机制说起,我们以HashMap为例进行探究,先上HashMap的put方法源码:(Talk is cheap,show me your code)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

从put方法源码中(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)我们可以看到,首先调用了key的hashcode方法进行hash码的计算,那么如果key重写了object的hashcode方法,那么在调用时肯定调用的是key重写后的hashcode方法,如果没有重写hashcode,那么调用的是Object的hashcode方法。
在得到hash值后我们可以从源码中看到通过tab.length&hash获取当前值存储在数组的下标。
在这里插入图片描述

因此我们得出第一个论点:往散列表(如hashmap)中插入一条数据时需要调用key的hashcode方法来计算出hash码,并进一步与数组的length进行&运算得出当前数据存储在数组中的位置。

那么通过hash和数组length求出所在数组下标后,如果当前下标中的数据为null,则直接把数据存储到当前下标中即可,如果当前下标中的数据不为null,则比较hash码是否相同,如果hash相同则进一步通过调用key的equals方法判断原来存储在该位置的key和当前key是否相同,如果相同则覆盖value,否则继续在邻接链表或者红黑树中继续找存储位置(此处属于hashmap深层部分,此处不展开将,请移步hashmap源码文章进行了解)。
在这里插入图片描述
因此我们得出的第二个论点是:当出现hash冲突时,需要调用key的equals方法来判断该位置存储的key和当前key是否相等,如果相等则替换value,不相等则在领接表或红黑树中继续查找

那么基于以上两个论点,我们可以发现hashmap中依赖对象的hashcode和equals方法来判断对象是否相等,因此我们需要保证在我们业务上完全相同的两个对象的hashcode相同并且调用equals方法返回true,其中一个不满足就会导致存入散列表出现问题。
假设我们需要实现这样一个场景:将Student对象作为key,School作为value存入hashmap中,在同一个学校中每个学生的学号是唯一的,因此判断两个学生对象是否相等,只需要判断学号是否相等即可,我们先只重写equals方法,Student类如下:

public class Student {
    private String studentId;

    private String grade;

    private String name;

    public Student(String id,String name)
    {
        studentId=id;
        this.name=name;
    }

    public String getStudentId() {
        return studentId;
    }

    public void setStudentId(String studentId) {
        this.studentId = studentId;
    }

    public String getGrade() {
        return grade;
    }

    public void setGrade(String grade) {
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

   @Override
    public boolean equals(Object s) {
        if(s instanceof Student) {
            Student s1=(Student)s;
            return  studentId.equals(s1.getStudentId());
        }
        return  false;
    }
}

那么我们将学号完全相同的两个student对象存入hashmap中,结果如下:
在这里插入图片描述
可见hashmap内部并没有将两个学号完全相同的student对象判定为相同的值,因为在没有重写hashcode的情况下,调用的Object的hashcode方法是对对象的内存地址进行hash运算,因此不同对象它们的hashcode不同(除非hash冲突)。那么我们也重写hashcode方法,重写后的student如下:

public class Student {
    private String studentId;

    private String grade;

    private String name;

    public Student(String id,String name)
    {
        studentId=id;
        this.name=name;
    }

    public String getStudentId() {
        return studentId;
    }

    public void setStudentId(String studentId) {
        this.studentId = studentId;
    }

    public String getGrade() {
        return grade;
    }

    public void setGrade(String grade) {
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

   @Override
    public boolean equals(Object s) {
        if(s instanceof Student) {
            Student s1=(Student)s;
            return  studentId.equals(s1.getStudentId());
        }
        return  false;
    }

    @Override
    public  int hashCode()
    {
        return studentId.hashCode();
    }
}

则再次运行结果如下:
在这里插入图片描述
可以看到,第二条数据覆盖了第一条数据,hashmap将学号相同的两个student对象判定为了完全相同的两个对象,达到了我们的需求。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪舟子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值