Object类的equals和hashCode方法有什么用?


一、背景

众所周知,在Java中Object类是所有类的父类,也就是说所有类都会有equals和hashCode方法。

二、equals()方法

1、代码实现

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

2、特点

equals()方法有以下几个特点:

  • 自反性x.equals(x)必须为true
  • 对称性x.equals(y)y.equals(x)的返回值必须相等
  • 传递性x.equals(y)truey.equals(z)也为true,那么x.equals(z)也必须为true
  • 非NULL:当x为NULL时,x.equals(y)会报NPE。当y为NULL时,x,equals(y)必须为false

三、hashCode()方法

1、代码实现

/** 返回对象内存地址,保证不同对象的hashCode也不同 **/
public native int hashCode();

2、特点

hashCode()方法有以下几个特点:

  • 如果x.equals(y),那么x和y的hashCode也一定相等
  • 如果!x.equals(y),那么x和y的hashCode有可能相等也有可能不相等(但是尽可能越散列越好)

3、作用

一般用在哈希表中,如HashSet、HashMap等

插入步骤:

  1. 调用hashCode()方法计算得到Object的哈希码
  2. 通过哈希码取余定位到下标
  3. 判断下标处是否为空,如果为空则直接插入,如果不为空则遍历链表(HashMap实现)
  4. 遍历链表调用equals()方法比较对象是否相等,如果相等则说明已存在,如果不相等则插入到链表尾部

四、String中equals和hashCode的实现

1、代码实现

public class String {
    private final char value[];
    private int hash; // Default to 0
    
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }

        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if(n == anObject.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                while(n-- != 0) {
                    if(v1[i] != v2[i]) {
                        return false;
                    }
                }
                return true;
            }
        }
        // 如果长度不一致则一定不相等
        return false;
    }
    
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            
            for(int i=0;i<value.length;i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        
        return h;
    }
}

2、解析

String代码实现可以看出以下几点:

  • String的底层存储char[]是final修饰的,说明String一旦创建就不可修改(比如String s = "abc",如果继续执行s = "bbq,那么将是会新建一个String对象,并且s指向新的String对象)
  • hashCode执行结果存到成员变量hash字段中,提高查询性能,避免每次都去计算哈希码
  • String的equals方法判断的是两个对象长度相同的前提下每个字符都相同(并不要求一定是同个String对象)

五、如何重写hashCode()

1、重写hashCode()的原则

  • 由于“如果x.equals(y),那么x和y的hashCode也一定相等”,所以如果我们重写了equals(),那么我们也应该重写hashCode()
  • hashCode()实现尽量分散,减少出现哈希冲突的可能
  • hashCode()实现尽量不要太复杂,避免影响性能

2、hashCode()重写方法

/** 尽可能用到equals()里所用到的所有对象变量,从而减少哈希冲突**/

六、Q&A

1、为什么String的hashCode计算用到了数字31?

String的hashCode()方法代码如下:

public class String {
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for(int i=0;i<value.length;i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }

        return h;
    }
}

可以得到String的hashCode=s[0]*31^(n-1)+s[1]*31^(n-2)+…+s[n-1]

为什么用到了数字31呢?原因是:

  • 31是质数,与其他数字相乘得到的计算结果唯一概率更大(比如62=312,但是64=322=164=88)
  • 但是质数中为什么选择31呢?因为质数越大,哈希冲突的概率就越小,所以在哈希冲突和性能的权衡下选择了31
  • JVM会对31做优化:31*i=(i<<5)-i

2、为什么要有hashCode?

hashCode()equals()其实都是用来判断两个对象是否相等。

在一些容器(如HashMapHashSet)中,比如要插入一个元素,就需要有以下步骤:

  1. 调用hashCode()计算对象的hashCode
  2. 判断集合中是否存在相同的hashCode,如果没有就可以直接插入,如果有则下一步
  3. 调用equals()判断两者是否相同,如果相同说明就可以直接覆盖,如果不相同则下一步
  4. (如HashMap)遍历拥有相同hashCode的列表直到尾部,如果有相同元素则直接元素,如果没有则插入到链表尾部

结论:hashCode提升了数据查询效率

3、为什么重写equals()时必须重写hashCode()方法?

因为equals()true的两个对象我们认为是相等的对象,那么他们的hashCode也必须是相等的。

如果我们重写了equals()但没重写hashCode()方法会发生什么?:以HashMap为例,可能会出现插入两个equals()返回true的对象在第二个插入时,发现hashCode与第一个对象不一致,导致命中了不一样的哈希槽,从而HashMap就出现了两个相等的对象,并不符合去重的特性(HashSet同理)

总结

  • equals()返回true时,hashCode()返回值也必须相等
  • hashCode()返回值相等时,equals()返回不一定为true

文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录