重写equals 和 重写hashcode的必要性和原理分析
首先上Object类的源码:
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
equals方法和hashcode方法是Object类的两个方法。
(1)equals():原始的Object类的equals方法比较两个对象的地址,如果地址相等,则返回true,否则返回false。因为JAVA所有的类继承于Object类,所以,java中的类均有equals方法,equals()默认比较对象地址。所以一般的对象用equals方法比较时,对应的类都会重写equals方法,例如比较对象的某个实例域。
(String类就重写了equals方法,比较的就是字符串内容,而不是String类对象的地址)
(2)hashCode():Object类的hashCode方法是一个本地方法(没有方法体,具体方法实现由非java代码实现(更贴近操作系统的语言),hashCode()默认返回的是对象的地址的hash值。)
所以默认的equals方法和hashCode方法是有某种对应关系的:
默认情况下,equals方法返回对象地址,hashCode返回对象地址的hashCode值,即:同一对象,equals返回true,hashCode返回值相同。
实例:
Student s1=new Student("jonsnow");
Student s2=s1;
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
运行结果:
但是在JAVA中,两个不同的对象的地址必然不同,所以如果不重写equals方法,那么普通的自定义类的实例的equals必然会返回false。但是一般情况下我们比较两个对象,并不是比较两个对象的地址(因为这意义不大),而是比较对象的某个实例域的值。例如Student类的stuName:
@Override
public boolean equals(Object s)
{
if(s instanceof Student)
{
Student s1=(Student)s;
if(this.stuName.equals(s1.stuName))return true;
else return false;
}
else
{
return false;
}
}
这个例子就是重写了equals方法,比较了Student类的stuName实例域。
那么现在就有一个问题了,当我只重写了equals,而没有重写hashCode,那么equals和hashCode的一致性便被破坏:
Student s1=new Student("jonsnow");
Student s2=new Student("jonsnow");
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
由结果可知,两个对象的equals判断相等,而他们的哈希值却不相等。
这里需要讨论的是,当这两者不一致会怎么样?
我认为,保持equals和hashCode一致是更符合逻辑的,并且是一个良好的开发习惯。但是,如果我只需要使用equals而不需要hashCode,那么重写equals不一定需要重写hashCode。然而,在某些特定情境下,尤其是集合框架(HashMap,HashSet等)的使用环境下,重写equals必须重写hashCode,否则会发生因为不一致而产生的混乱。
首先,我们需要了解HashMap的存储原理(扩容等不详细分析):
HashMap是一个数组加链表的结构,即以数组实现散列结构,每个数组元素是一个链表,存储相同hash值的基本元素。
每个黑色的圆代表存储在HashMap里的每个基本元素,例如:
HashMap<Student,Integer> hashMap=new HashMap();
Student a=new Student("abc");
hashMap.put(a,1);
将Student类的实例a存储在HashMap中,HashMap首先会计算a的hashCode,再经过一次hash,得到存储在数组中的位置。当该位置已有对象s存在时,会调用equals方法比较两个对象是否相等,如果相等,就更新原对象s0的值为s1的值,如果不相等,就在链表后添加新对象s1。
实例:
(Student类未重写equals方法和hashCode方法)
HashMap<Student,Integer> hashMap=new HashMap();
Student s=new Student("abc");
Student a=new Student("abc");
hashMap.put(s,1);
hashMap.put(a,2);
System.out.println("两个对象的equals返回值为"+s.equals(a));
System.out.println("两个对象的hashCode值分别为 s:"+s.hashCode()+",a:"+a.hashCode());
System.out.println("hashMap的大小为:"+hashMap.size());
System.out.println("对象s的值为:"+hashMap.get(s));
System.out.println("对象a的值为:"+hashMap.get(a));
结果符合预期的结果,两个对象的地址不同,所以他们的equals方法返回为false,hashCode值不同,两个对象都可以放在HashMap中。
现在,重写Student类的equals方法:
@Override
public boolean equals(Object s)
{
if(s instanceof Student)
{
Student s1=(Student)s;
if(this.stuName.equals(s1.stuName))return true;
else return false;
}
else
{
return false;
}
}
equals比较两个对象的stuName域的值。
那么,如果代码不变,当a对象放入HashMap中时,a等于s(a.equals(s)==true),应当更新s的值为2,并且HashMap的size为1。然而实际结果为:
HashMap将a对象作为一个新的基本元素添加进去了。
因为a对象和s对象的Hash值(地址不同,所以hash值不同)不同,HashMap将a元素当做了新的元素。
那么,现在同时重写hashCode方法,返回字符串的hash值(当字符串相等时,hash值也相等)。
@Override
public int hashCode()
{
return this.stuName.hashCode();
}
@Override
public boolean equals(Object s)
{
if(s instanceof Student)
{
Student s1=(Student)s;
if(this.stuName.equals(s1.stuName))return true;
else return false;
}
else
{
return false;
}
}
运行结果:
那么现在,结果符合预期。a对象与s对象相等,那么将a对象添加到HashMap中,会更新s对象的值。
所以,一般情况下,重写equals尽量重写hashCode,在使用HashMap等集合框架时,重写equals必须重写hashCode。