equals&hashCode
平时我们写一个java对象,很多时候会使用IDE自动生成equals方法和hashcode,那么这个两个方法到底有什么用?为什么两个要同时重写?如果不使用自动生成,该如何实现自己的equals和hashcode方法?
查看jdk发现equals和hashcode都是Object的方法,因此只有对象类型才能调用这两个方法:
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
equals
equals一般可认为从语义上比较两个对象是否相等,Object实现中使用==直接比较两个对象的地址是否相同,两个对象==判断的是地址是否相同,对原生类型int/double则是比较值是否相同。默认实现中对象只有equals自身才会返回true, 实际应用中我们一般是从语义层面判断两个对象是否相等,比如两个对象包含的成员之间equals都返回true,则认为两个对象equals也应该返回true, 因此,一般我们需要重写equals方法,使用业务场景中比较逻辑判定两个对象是否相同。
hashCode
hashCode返回值是int, 声明为native,在jdk中并未实现,由jvm实现者自行定义,既然是自行定义,没有规定如何实现,但是根据hashCode的作用对其返回值肯定还是有些规定的。hashCode定义了一个对象到一个int整数值得映射,主要用于hash table中作为key进行对象的定位查找。我们知道Hash Table的时间复杂度为O(1),最坏情况却为O(n),为了保证hash table的效率,应该较少碰撞的发生概率,因此hashCode函数返回值应该尽量分散,同一个对象的hashCode应该是幂等的,不同对象的hashCode应该尽量不同,但也有可能两个不同对象的hashCode相等,hashCode的实现应该尽量减少此种情况发生的概率。
equals & hashCode
一般我们看到equals和hashCode都是同时出现的,重写了equals方法的同时也要重写hashCode.为什么会这样?
你可以理解为这是java的规定,java规定objectA.equals(ObjectB) == true时,objectA.hashCode() == objectB.hashCode(),反之则不一定成立,因此重写equals后,为了保证此规则依然成立,一般也需要重写hashCode方法。
也许你还想问为什么有这样的规定?Java规范的制定者也许是从equals和hashCode的使用场景角度考虑,只有遵循这样的规定,JDK源代码中依赖这条规范的实现才合乎现实语义逻辑,如果破坏这条规则则会得到莫名奇怪的结果。
package com.test1;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public boolean equals(Object obj) {
return this.name.equals(((People)obj).name) && this.age== ((People)obj).age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("A", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("A", 12)));
}
}
以上代码最后打印出null,就是因为HashMap的实现依赖hashCode方法,如果我们不遵循这条规则,HashMap也就不会返回给我们预期的结果。
自定义hashCode
既然hashCode的实现应该减少碰撞,说明这是一个数学问题,在工程上有一些标准成熟的算法,这也是IDE能为我们自动生成hashCode方法实现的原因。
通过JDK源码我们可以看看各种Class的hashCode具体实现:
//Interger直接返回对应的int数
public int hashCode() {
return value;
}
//Long则通过两个32bit异或操作强转成int
public int hashCode() {
return (int)(value ^ (value >>> 32));
}
//String通过一个特殊的算法合并了每个字符后计算出一个Int值
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;
}
jdk开发者使用了很好的方式实现各个类hashCode, 对我们自定义的类,则可按以下方式自定义一个合适的hashCode:
- 把某个非0的常数值,比如17,保存在一个名为result的int类型的变量中。
-
对于对象中的每个域f计算int类型hash值c:
- 如果该域是boolean类型,则计算(f?1:0)
- 如果该域是byte、char、short或者int类型,则计算(int)f
- 如果该域是long类型,则计算(int)(f^(f>>>32))
- 如果该域是float类型,则计算Float.floatToIntBits(f)
- 如果该域是double类型,则计算Double.doubleToLongBits(f),然后重复第三个步骤。
- 如果该域是一个对象引用,并且该类的equals方法通过递归调用equals方法来比较这个域,同样为这个域递归的调用hashCode,如果这个域为null,则返回0。
- 如果该域是数组,则要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。
-
把上面计算得到的hash值c合并到result中
- result = 31*result + c
参考http://www.importnew.com/8189.html
是不是很麻烦,还是自动生成吧,如果觉得自动生成的太冗长或不是想要的,可以借助Apache commons HashcodeBuilder或Guava Hash中各种HashFunction计算hashCode。
推荐阅读