java的equals和hashCode

java中的equals和hashCode方法

一.equals方法

java中的equals方法是Obeject类的方法,用于比较两个对象是否相同。java8中实现源码如下:

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

可以看到,Object类默认的equals方法就是使用==对两个对象进行比较,而==本质上是比较两个对象的地址是否相同,所以,在没有重载equals方法时,类的equals方法与==相同,就是比较两个对象的地址是否相同。

二.hashCode方法

java中hashCode方法是用来返回一个特殊的整数值(哈希码),官方对一个对象的hash码有以下一般规定

Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.

有如下关键点

  1. 在一个java程序执行期间,同一个对象的hashCode()方法的返回值应该相同,但在不同的时间执行相同程序,其hashCode()返回值不一定相同。

  2. 如果有两个对象a,b,使用a.equals(b)方法返回true,一般要求其hashCode()返回的整数应该一致,即a.hashCode() == b.hashCode()

  3. 如果两个对象a,b,使用a.equals(b)方法返回false,不要求hashCode()返回的整数不一致,即a.hashCode() != b.hashCode()不一定成立。(这种情况即出现了hash冲突,解决方法有拉链法,再哈希法,开放定址法,建立公共溢出区,java的HashMap处理hash冲突使用的是拉链法)。

hashCode默认的生成方法如下:

public native int hashCode();

native关键字用来嵌入其他语言,意思是这个方法是用其他语言(如C和C++)实现的,其具体实现在jvm源代码中,其具体实现方式可以参考博文java Object的hashCode方法的计算逻辑_object计算hashcode的方法_磨唧的博客-CSDN博客,简单来说,就是根据地址生成一个特定的整数值,而且会尽量不重复。

三.equals方法和hashCode方法的重写

1.equals方法的改写

当我们想让两个对象相等时是其中的两个子段相等时,我们会重写其equals方法。比如:

class Stu{
    public int id;
    public Stu(int id){
        this.id = id;
    }
}

在此时,有两个Stu对象a,b,其id相等时,使用equals比较会不相等

Stu a = new Stu(1);
Stu b = new Stu(1);
boolean c = a.equals(b); //此时c为false,因为a==b会返回false,两者地址不相同

我们想实现在id相同时,equals方法返回true,就要重写其equals方法

class Stu{
    public int id;
    public Stu(int id){
        this.id = id;
    }

    @Override
    public boolean equals(Object s){
        if(s instanceof Stu){
            Stu temp = (Stu) s;
            if(temp.id == this.id){
                return true;
            }

        return false;
        }
        
        return false;
    }

}

此时,再次用equals比较a,b,就会返回true了。

Stu a = new Stu(1);
Stu b = new Stu(1);
boolean c = a.equals(b); //此时c为true,因为a,b的equals方法被重写了。

2.hashCode方法的改写

2.1改写原因

但在这时候,会出现一个新问题,此时a,b的hashCode返回值是不相等的,违背了规定

如果有两个对象a,b,使用a.equals(b)方法返回true,一般要求其hashCode()返回的整数应该一致,即a.hashCode() == b.hashCode()

这就逻辑上就是说,a,b是同一个东西,但却被放在了不同的位置(hash码本质就是其放的位置),这显然不合理,会导致HashSet在增加元素时不能自动去重,比如:

public class Test{
    public static void main(String[] args) {
        Set<Stu> set = new HashSet<>();
        Stu s1 = new Stu(2);
        Stu s2 = new Stu(2);
        set.add(s1);
        set.add(s2);
        System.out.println(set.size());
    }
}

class Stu{
    public int id;
    public Stu(int id){
        this.id = id;
    }

    @Override
    public boolean equals(Object s){
        if(s instanceof Stu){
            Stu temp = (Stu) s;
            if(temp.id == this.id){
                return true;
            }

        return false;
        }
        
        return false;
    }
}

此时你会发现,set的大小为2,意思是s1和s2都在里面,这是不合理的,因为set在加元素时会加相同的元素时自动去重,但s1和s2虽然相同,但却没有被自动去重。这是为什么呢,我们看看源码。

HashSet的add方法如下

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

我们再找此处使用的put方法如下:

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

再找此处的putVal方法

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;
}

具体代码很复杂,不太好分析,但是,我们注意到,其中有这么一行代码

if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

可以看到,要满足这个if条件,有以下要求

  • 两者hash值相同,即p.hash == hash
  • 两者==返回true或是equals()方法返回true,即(k = p.key) == key || (key != null && key.equals(k)))

所以可以看出,往HashSet中加元素时,如果其哈希值相同 && (==返回true || equals()方法返回true),就会被自动去重,否则不会。因此,在改equals方法时,应该改写hashCode方法,使得equals返回true的方法hashCode返回值相同。

2.2改写方法

如何改写该方法呢,有一个简单改法

@Override
public int hashCode(){
	return 1;
}

这样,所有对象的hashCode都会返回1,满足了hash码的一般规定。

但是,这样做会大大降低HashSet的效率,因为本质上就是将所有元素的索引都设为1,然后使用拉链法解决冲突,HashSet就退化为了List。为了解决这个问题,我们使用如下方式重写

@Override
public int hashCode(){
    int prime = 31;
    int result = 1;
    Integer i = id;
    result = result * prime + (i == null ? 0 : i.hashCode());
    return result;
}

就是把类的所有用于equals比较的字段的哈希码不断进行相加再乘31的操作。这样,保证了不同对象有不同的hash码,相同对象有相同的hash码。为啥要乘31呢,直接相加不也能满足上面的性质吗?主要有以下原因:

首先为了尽量让产生hashcode保持唯一,所以一定使用一个素数来做系数(这里的31)
但为什么是31而不是别的素数呢?
因为31属于一个特殊的质数
任何数 乘以 31 就等于 这个数 * 2 的5次方 - 这个数本身
<<左移几位 表示 乘以 2 的几次方
>>右移几位 标识 除以 2 的几次方
n * 31 等价于 (n * 2 * 2* 2 * 2 * 2 - n) (n << 5) - n

(引自在Java中重写hashCode()方法_java重写hashcode方法_宇智波的头头的博客-CSDN博客)

不过,在java7以后,java贴心地给我们封装了这个改写方法在Objects.hash(Object... values)中,其参数为所有在equals中比较的字段。源码如下

public static int hash(Object... values) {
	return Arrays.hashCode(values);
}

我们再找Arrays.hashCode(values)的源码,

public static int hashCode(Object a[]) {
    if (a == null)
    	return 0;

    int result = 1;

    for (Object element : a)
    	result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

发现逻辑和我们的一样。

因此上面的改写就可以写成

@Override
public int hashCode(){
	return Objects.hash(id);
}

3.改写结果

在equals和hashCode改写完后,具体代码如下

public class Test{
    public static void main(String[] args) {
        Set<Stu> set = new HashSet<>();
        Stu s1 = new Stu(2);
        Stu s2 = new Stu(2);
        set.add(s1);
        set.add(s2);
        System.out.println(set.size());
    }
}

class Stu{
    public int id;
    public Stu(int id){
        this.id = id;
    }

    @Override
    public boolean equals(Object s){
        if(s instanceof Stu){
            Stu temp = (Stu) s;
            if(temp.id == this.id){
                return true;
            }

        return false;
        }
        
        return false;
    }

    @Override
    public int hashCode(){
        return Objects.hash(id);
    }

}

此时,我们再运行代码就会发现set的size为1,只有一个元素被加进来了。

posted @ 2023-06-14 12:48  tryingWorm  阅读(77)  评论(0编辑  收藏  举报