为什么equals和hashCode方法需要同时重写

注意:如果是在写算法题的话,假设只涉及到比较两个对象是否相等,用不到hashCode的话,是可以不用同时重写的。当然,这样并不建议。

一、原生equals和hashCode

1.原生的equals方法用于严格判断一个对象是否相等

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

2.原生的hashCode方法返回根据内存地址换算出来的一个值

  • As far as is reasonably practical, the {@code hashCode} method defined

    by class {@code Object} returns distinct integers for distinct objects.

二、为什么需要重写equals

在实际业务中,我们判断对象是否相等往往并不是从严格意义上的角度出发,比如判断两个Product对象是否相等,我们会以成员变量name判断其是否相等,此时就需要重写equals

public boolean equals(Object obj) {
	  if(obj==null)
		  return false;
	  if(!(obj instanceof Product))
		  return false;
	  if(this==obj)
		  return true;
	  
	  Product pro = (Product)obj;
	  if(this.name.equals(pro.name))
		  return true;
	  return false;
	}

三、重写了equals,为何要重写hashCode

  1. 如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。

  2. 如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一个对象的hashCode方法,不要求必须产生不同的整数结果。然而,程序员应该意识到这样的事实,对于不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。


    ------------------- 引自《Effective Java》中有关Object.hashCode的通用约定

根据上述第一条:相等的对象必须具有相等的散列码,即hashCode。

反之,若不重写hashCode,原生的方法通过对象地址产生散列码,不会相等。

四、HashMap和HashSet中的HashCode

1.HashMap中的hash方法:

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

上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数。

若key为自定义类,则调用自定义类中的hashCode函数。

2.HashMap的底层处理机制:

HashMap的底层处理机制是以数组的方法保存放入的数据的(Node<K,V>[] table),其中的关键是数组下标的处理。

数组的下标是根据传入的元素hashCode方法的返回值再和特定的值异或决定的。

3.HashSet是通过HasMap来实现的:

HashMap的输入参数有Key、Value两个组成,在实现HashSet的时候,保持HashMap的Value为常量,相当于在HashMap中只对Key对象进行处理。

4.如果不重写hashCode:

若不重写hashCode可能导致HashSet、HashMap不能正常的运作。

另外,如果我们将某个自定义对象存到HashMap或者HashSet及其类似实现类中的时候, 如果该对象的属性参与了hashCode的计算,那么就不能轻易修改该对象参数hashCode计算的属性了

有可能会移除不了元素,导致内存泄漏。

五、Java源码中的hashCode

以下是StringUTF16类中的hashcode方法,直接作用于String类

 public static int hashCode(byte[] value) {
        int h = 0;
        int length = value.length >> 1;//右移一位,相当于 /2
        for (int i = 0; i < length; i++) {
            h = 31 * h + getChar(value, i);//神奇的数字31
        }
        return h;
    }

注:为何用31——任何数n*31都可以被jvm优化为(n<<5)-n,移位和减法的操作效率比乘法的操作效率高很多

17、31、33、63、127和129等相对其他的奇数的一个很明显的优势是,由于这些奇数与16、32、64、128只相差1,可以通过移位(如1 << 4 = 16)和加减1来代替乘法,速度更快。

​ -------------------------------------------------------- 引自他人博客(其中还有实验验证)

六、自定义类如何重写hashCode

要重写自己的hashCode方法并没有什么绝对正确的答案,但是我们的目标是:不相等的对象尽可能有不同的hashCode,而且必须满足的一个通用约定是:相等的对象应该具有相同的hashCode。下面介绍一种hashCode的实现方式,这种实现方式对一般的程序来说足够了,至于如何实现更完美的hashCode方法就留给数学家或者理论家去讨论吧。

以下为
《Effective Java》中提出的一种简单通用的hashCode算法

第一步:定义一个初始值,一般来说取17

int result = 17;

第二步:分别解析自定义类中与equals方法相关的字段(假如hashCode中考虑的字段在equals方法中没有考虑,则两个equals的对象就很可能具有不同的hashCode)

情况一:字段a类型为boolean 则[hashCode] = a ? 1 : 0;

情况二:字段b类型为byte/short/int/char, 则[hashCode] = (int)b;

情况三:字段c类型为long, 则[hashCode] = (int) (c ^ c>>>32);

情况四:字段d类型为float, 则[hashCode] = d.hashCode()(内部调用的是Float.hashCode(d), 而该静态方法内部调用的另一个静态方法是Float.floatToIntBits(d))

情况五:字段e类型为double, 则[hashCode] = e.hashCode()(内部调用的是Double.hashCode(e), 而该静态方法内部调用的另一个静态方法是Double.doubleToLongBits(e),得到一个long类型的值之后,跟情况三进行类似的操作,得到一个int类型的值)

情况六:引用类型,若为null则hashCode为0,否则递归调用该引用类型的hashCode方法。

情况七:数组类型。(要获取数组类型的hashCode,可采用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + … + s[n-1], 该方法正是String类的hashCode实现所采用的算法)

第三步:对于涉及到的各个字段,采用第二步中的方式,将其依次应用于下式:

result = result * 31 + [hashCode];

补充说明一点:如果初始值result不取17而取0的话,则对于hashCode为0的字段来说就没有区分度了,这样更容易产生冲突。比如两个自定义类中,一个类比另一个类多出来一个或者几个字段,其余字段全部一样,分别new出来2个对象,这2个对象共有的字段的值全是一样的,而对于多来的那些字段的值正好都是0,并且在计算hashCode时这些多出来的字段又是最先计算的,这样的话,则这两个对象的hashCode就会产生冲突。还是那句话,hashCode方法的实现没有最好,只有更好。

​ ---------------------------------------------------------------------------------------引自他人博客

七、总结:equals和hashCode判断元素相等

1、hashcode相等,两个对象不一定相等,需要通过equals方法进一步判断;

2、hashcode不相等,两个对象一定不相等;

3、equals方法为true,则hashcode肯定一样;

4、equals方法为false,则hashcode不一定不一样;

理解为主,记忆为辅

©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页