在Java中,hashCode()和equals()是基类Objcet的两个方法,它们在Object中的实现十分简单,如下:hashCode函数主要功能是返回一个Object对象的物理地址,equals则比较两个Object的引用地址是否相等。
而在实际应用中,那些继承自Object往往需要根据实际情况重写equals,例如String类重写了equals方法,使得两个String类型的对象相等的条件变成了对象内容相同,而不是简单的引用的地址相等,这是具有实际意义的。
在讨论这个问题之前,我们还有需要了解一点预备知识,我们就经常看到一句所谓的名言“两个对象equals,hashcode一定相等;两个对象hashcode相等,但是不一定equals”。举个例子就明白了,String类重写equals了,两个相等的条件是逐个比较两个字符串,如果两个字符串每一个字符都相等,则equals。而hashcode函数则是实现了一个复杂的hash函数(根据字符串的长度而定的一个多项式函数),可以这么类比,equals是忠实的比较两个对象,而hashcode则是使用一个hash函数,将两个对象作为输入条件,而最终比较的是hash函数的输出结果。众所周知,一个好的hash函数会使得不同的输入有不同的输出,但是也无法避免的可能存在不同的输入对应相同的输出,这也被称为Hash冲突。另外,作为一个函数,相同的输入是不可能得到不同的输出的,反过来说,如果两个对象经过hash函数运算输出不同,则它们必然输入(即对象本身)不相等(因为这俩是逆否命题)。
上面关于重写equals和hashcode方法真正派上用场的地方在于集合类的使用。集合类中,要求对象是不能有相同的元素,这和代数中的要求是一样的,因此每次向集合对象中添加元素时,会首先判断集合中是否已经包含了该对象,它是这样判断的:
条件1.先比较两个对象的hashcode,如果等则进入条件2比较。如果不等直接则返回false。(具体参见HashMap.get()方法的源码)
条件2.比较equals。
于是存在这么一种情况,如果HashSet对象中的元素是Student对象,定义如下:
Public class Student{
String name;
int age;
}
这时,我们在向HashSet对象插入Student对象时,HashSet对象会将这个student对象和集合中已经存在的所有student对象逐个比较。方法如前所述,如果student不重写equals方法,则使用继承自Object的equals方法,显然这是不符合情景的,因此重写equals方法:
@Override
publicboolean equals(Object obj) {
Student stu = (Student)obj;
if(this.getName().equals(stu.getName()) && this.getAge() == stu.getAge()){
returntrue;
}else{
returnfalse;
}
}
此时测试代码:
Student stu1 = new Student();
Student stu2 = new Student();
stu1.setName("Ann");
stu1.setAge(2);
stu2.setName("Ann");
stu2.setAge(2);
System.out.println("stu1.equals(stu2):" + stu1.equals(stu2));
输出结果为:stu1.equals(stu2):false。未复写当然为false.
此时复写hashcode方法,使得内容相同的Student对象具有相同的hashcode值,当然为了测试集合类中contain函数是否如前所说是先比较对象的hashcode值,再比较equals函数,可以改写hashcode函数使得它返回相同的值,此时可以通过改变equals复写方式,从而看出contain比较顺序确如之前所说。
@Override
publicint hashCode() {
returnthis.name.hashCode() + age;
}
因此在hashset这个例子当中,由于首先比较两个对象的hashcode,再比较equals,因此,如果不重写hashcode函数的,即使两个对象的内容相等,那么由于他们的hashcode不等,这样他也会被加入到Hashset当中,这样的话,hashset对象中即存在了相同的对象,这就不对了。在上面,我提到了内容相等,这里其实也暗含了复写equals方法,但是,实际应该先复写equals还是先复写hashcode呢,或者说是应该先有复写equals的动机还是先有复写hashcode的动机,学生我也不明白,听起来似乎有点先有鸡还是先有蛋,其实没关系,这其实不是重点,只要记住一点就是了,复写equals的时候我同时复写了hashcode方法,并保证equals相等的对象hashcode相等就可以了。
另外,在别的论坛上也看到说:Hashcode中有一个常用规定,就是equals相等的对象hashcode也要相等。
于是得出一个重要结论,同时复写equals函数和hashcode函数,也别管谁先谁后的问题。重写equals函数主要基于业务逻辑需要,而重写hashcode函数更侧重语法需要。另外,这个问题的核心其实在于“哈希冲突”,就是前面提到的对于hash函数,可能存在对于不同的输入得到相同的输出,这是一个数学问题,因此需要构造足够强大的哈希函数才能有效减少这个问题,数学上讲哈希冲突只能尽量避免,无法消除,这是个概率问题。
=================
1.当将一个自定义类放入 hashMap,hashSet,hashTable中时,一定要重写hashcode和equals方法。
2.重写equals方法的原则:
离散数学中定义的equals的原则是:
1. 判断是否等于自身;2. 使用instanceof运算符判断 other 是否为同类型的对象;3. 比较类中你自定义的数据域,一个都不能少.
@Override
public boolean equals(Object other) {
System.out.println("equals method invoked!");
if(other == this)
return true;
if(!(other instanceof Coder))
return false;
Coder o = (Coder)other;
return o.name.equals(name) && o.age == age;
}
3.重写hashcode方法的原则:(注:此处是重写hashcode的方法之一)
public int hashCode() {
int result = 17; //任意素数
result = 31*result +c1; //c1,c2是什么看下文解释
result = 31*result +c2;
return result;
} 其中c1,c2是我们生成的你要计算在内的字段的代码,生成规则如下:
如果字段是boolean 计算为(f?1:0);
如果字段是byte,char,short,int则计算为 (int)f;
如果字段是long 计算为 (int)(f^(f>>32));
如果字段是float 计算为 Float.floatToLongBits(f);
如:
@Override
public int hashCode() {
int result = 17;
result = result * 31 + name.hashCode();
result = result * 31 + age;
return result;
}