注意:如果是在写算法题的话,假设只涉及到比较两个对象是否相等,用不到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
如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
如果两个对象根据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不一定不一样;
理解为主,记忆为辅