说明:
本系列博客是本人在工作中遇到的一些问题的整理,其中有些资料来源网络博客,有些信息来自出版的书籍,掺杂一些个人的猜想及验证,总结,主要目的是方便知识的查看,并非纯原创。本系列博客会不断更新。原创不容易,支持原创。对于参考的一些其他博客,会尽量把博客地址列在博客的后面,以方便知识的查看。
本篇博客可以看做是《Effective Java中文版第2版》第三章(对于所有对象都通用的方法)第九条(覆盖equals时总要覆盖hashCode)的读书笔记,其中掺杂了一些个人的想法及验证。
我们来看下Object类中hashCode方法的实现:
public native int hashCode();
这是一个本地方法
java规范对其描述是这样的:
1、在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
2、如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生相同的整数结果。
3、如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中国任意一个对象的hashCode方法,不一定产生不同的整数结果。但是给不相等的对象产生不同的整数结果,有可能提高散列表的性能。
书中的一个例子:
/**
* Created with IntelliJ IDEA.
* User: [email protected]
* Date: 2014-08-20 13:27
* Description:
*/
public class PhoneNumber {
private short areaCode;
private short prefix;
private short lineNumber;
public PhoneNumber(int areaCode,int prefix,int lineNumber){
this.areaCode = (short)areaCode;
this.prefix = (short)prefix;
this.lineNumber = (short)lineNumber;
}
}
此处没有覆盖hashCode和equals方法,看下测试:
/**
* Created with IntelliJ IDEA.
* User: [email protected]
* Date: 2014-08-19 23:41
* Description:
*/
public class HashCodeTest {
public static void main(String[] args) {
PhoneNumber phoneNumber1 = new PhoneNumber(1,2,3);
PhoneNumber phoneNumber2 = new PhoneNumber(1,2,3);
System.out.println("phoneNumber1.hashCode:" + phoneNumber1.hashCode());
System.out.println("phoneNumber2.hashCode:" + phoneNumber2.hashCode());
}
}
控制台输出:
phoneNumber1.hashCode:24355087
phoneNumber2.hashCode:5442986
类的不同实例,他们的hashCode不同
我们来加上equals方法:
@Override
public boolean equals(Object obj) {
if (obj == this){
return true;
}
if (!(obj instanceof PhoneNumber)){
return false;
}
PhoneNumber p = (PhoneNumber)obj;
return this.areaCode == p.areaCode
&& this.prefix == p.prefix
&& this.lineNumber == p.lineNumber;
}
看下如下测试:
public class HashCodeTest {
public static void main(String[] args) {
PhoneNumber phoneNumber1 = new PhoneNumber(1,2,3);
PhoneNumber phoneNumber2 = new PhoneNumber(1,2,3);
System.out.println("phoneNumber1.hashCode:" + phoneNumber1.hashCode());
System.out.println("phoneNumber2.hashCode:" + phoneNumber2.hashCode());
System.out.println("------------------");
System.out.println("phoneNumber1.equals(phoneNumber2):" + phoneNumber1.equals(phoneNumber2));
Map<PhoneNumber,String> map = new HashMap<PhoneNumber, String>();
map.put(phoneNumber1,"zhangsan");
System.out.println("map contains phoneNumber1:" + map.containsKey(phoneNumber2));
String name = map.get(phoneNumber2);
System.out.println("name:" + name);
}
}
创建PhoneNumber 的两个实例,先比较他们的equals,然后将第一个实例加入hashMap,然后判断HashMap的containsKey方法及get方法(入参是第二个实例),控制台输出如下:
phoneNumber1.hashCode:24355087
phoneNumber2.hashCode:5442986
------------------
phoneNumber1.equals(phoneNumber2):true
map contains phoneNumber1:false
name:null
导致这个原因是因为覆盖了equals方法,没有同时覆盖hashCode方法,HashMap实现在put、get、containsKey等方法时会计算key的hash值,具体HashMap的原理可以参看博客:
http://www.cnblogs.com/xwdreamer/archive/2012/06/03/2532832.html
已经讲得很详细了,不重复劳动了。
书中提出的一种简单的hashCode生成方式如下:
1、把某个非0的常数值,比如说17,保存在一个名为result的int类型的变量中;
2、对于对象中的每个关键域f(equals方法中涉及的每个域),完成以下步骤:
a.为该域计算int类型的散列码c:
ⅰ.如果该类型是boolean类型,则计算(f ? 1 : 0);
ⅱ.如果该域是byte、char、short、int类型,则计算(int)f;
ⅲ.如果该域是long类型,则计算(int)(f ^ (f >>> 32));
ⅳ.如果该域是float类型,则计算Float.floatToIntBits(f);
ⅴ.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤
2.a.ⅲ,为得到的long类型的值计算散列值;
ⅵ.如果该域是一个对象引用,并且该类的equals方法通过递归的调用equals的方式
来比较这个域,则同样为这个域递归的调用hashCode,如果这个域的值为null,则
返回0;
ⅶ.如果该域是一个数组,则要把每一个元素当做单独的域来处理。如果数组域中
的每个元素都很重要,可以使用Arrays.hashCode方法。
b.按照如下公式,把2.a中计算得到的散列码c合并到result中:
result = 31 * result + c;
3、返回result。
以上规则能记住当然最好,但是工作中我们用的IDE往往为我们提供了这些功能,以下代码是在IDEA中自动生成的:
@Override
public int hashCode() {
int result = (int) areaCode;
result = 31 * result + (int) prefix;
result = 31 * result + (int) lineNumber;
return result;
}
跟以上规则是类似的。
再来运行下之前的测试:
phoneNumber1.hashCode:1026
phoneNumber2.hashCode:1026
------------------
phoneNumber1.equals(phoneNumber2):true
map contains phoneNumber1:true
name:zhangsan
我们来看下如下的测试:
/**
* Created with IntelliJ IDEA.
* User: [email protected]
* Date: 2014-08-20 15:25
* Description:
*/
public class PhoneNumberTest {
public static void main(String[] args) {
PhoneNumber phoneNumber1 = new PhoneNumber(1,2,3);
Map<PhoneNumber,String> map = new HashMap<PhoneNumber, String>();
map.put(phoneNumber1,"zhangsan");
System.out.println("map contains PhoneNumber(1,2,3) before update:" + map.containsKey(new PhoneNumber(1,2,3)));
System.out.println("----------------------");
for (PhoneNumber phoneNumber : map.keySet()){
System.out.println("before update hash code:" + phoneNumber.hashCode());
phoneNumber.setAreaCode((short) 4);
System.out.println("after update hash code:" + phoneNumber.hashCode());
}
for (PhoneNumber phoneNumber : map.keySet()){
System.out.println(phoneNumber.getAreaCode() + " " + phoneNumber.getPrefix() + " " + phoneNumber.getLineNumber() + " " + phoneNumber.hashCode());
}
System.out.println("map contains PhoneNumber(1,2,3) after update:" + map.containsKey(new PhoneNumber(1,2,3)));
System.out.println("##############");
System.out.println("map contains PhoneNumber(4,2,3) after update:" + map.containsKey(new PhoneNumber(4,2,3)));
}
}
将phoneNumber1 加入HashMap,然后判断map.containsKey(new PhoneNumber(1,2,3)),然后对HashMap遍历,修改key的areacode属性,以改变key的hashCode值,然后再去判断map.containsKey(new PhoneNumber(1,2,3))和map.containsKey(new PhoneNumber(4,2,3))
看下输出结果:
map contains PhoneNumber(1,2,3) before update:true
----------------------
before update hash code:1026
after update hash code:3909
4 2 3 3909
map contains PhoneNumber(1,2,3) after update:false
##############
map contains PhoneNumber(4,2,3) after update:false
后面两次比较都是返回false
原因:
for (PhoneNumber phoneNumber : map.keySet()){
System.out.println("before update hash code:" + phoneNumber.hashCode());
phoneNumber.setAreaCode((short) 4);
System.out.println("after update hash code:" + phoneNumber.hashCode());
}
这段代码执行过后,因为map中key的areaCode值已经修改,在判断map.containsKey(new PhoneNumber(1,2,3))时,由于equals方法返回false导致contains方法返回false;而在判断map.containsKey(new PhoneNumber(4,2,3))返回false是因为map的key的hashCode与new PhoneNumber(4,2,3)的hashCode值不一样,这点在前文提到的博客中有说明,map中的entry中的key会缓存计算后的hash值,详细信息可以参看上文提到的博客。
hashCode和equals方法还是很重要的,在理解hibernate的数据库一致性与实体一致性时有涉及到。