作者:gnuhpc
出处:http://www.cnblogs.com/gnuhpc/
这个话题还是从一个有问题的代码中引申出来的,原代码如下:
import java.util.*;
class TreeSetTest
{
public static void main(String[] args)
{
HashSet hs=new HashSet();
Student st1=new Student(1,"zhao1");
Student st2=new Student(1,"zhao1");
hs.add(st1);
hs.add(st2);
System.out.println(hs);
}
}
class Student
{
public Student(int num,String name)
{
this.num=num;
this.name=name;
}
public int hashCode()
{
return new Integer(num).hashCode();
}
public boolean equals(Student st)
{
if (name==st.name) return true;
else return false;
}
public String toString()
{
return "student "+num+" name:"+name;
}
int num;
String name;
}
为什么st1和st2两个对象内容完全一样,却还能插入到一个set中呢,set不是不能有重复的对象吗?
这段程序有两个主要问题,就要先从Java中两个面向对象的基本含义说起了:
JAVA中的重载overload:
只要是一个类以及其父类里有的两个函数有相同的名字但是不同的参数列表 (包括参数类型,参数个数,参数顺序3项中的一项或多项)。重载可以在单个类或者两个具有继承关系的类中出现。 是实现类的多态性的一种重要方式。JAVA中的覆盖override :
覆盖只会在类继承的时候才会出现,覆盖要求两个函数的名字和参数列表都完全一样。
在HashSet判断是不是重复元素时是使用了equals方法,不过请注意自定义的这个类实际继承了Object类,而Object类中equals方法的定义如下:
public boolean equals(Object o)
这么说,这段程序中定义的equals方法是对Object中的equals方法的重载,而不是覆盖,那么在HashSet判断重复元素时,实际调用的就是Object.equals 方法,自然是true。
所以该程序第一个需要修改的地方就是equals方法:我们要的是覆盖不是重载,为了防止这样问题,可以加上annotation让Eclipse自己去判断。
public boolean equals(Object st)
{
Student tempStudent= (Student) st;
if (name==tempStudent.name) return true;
else return false;
}
另外,该程序段在自定义类的hashCode方法和equals并不一致,前者是用num作为hashCode方法的依据,而后者是用name作为判断是不是相同的依据。
1)利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的,在 hashCode中仅在两个对象有着相同hashCode()的时候才会调用equals方法去比较,因为hashset内部采用对某个数字n进行取余的 方式对哈希码进行区域划分,也就是说即使哈希码不同,他们也可能被划分在同一个区域。在添加数据时,首先计算hashcode(String 对象的哈希码根据以下公式计算: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 注:使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂。(空字符串的哈希值为 0)),发现在一个区域的才会使用equals方法去一一比较同一区域的对象是否相同,否则直接插入。
| | |
| 区域1 | 区域2 | ……
| | |
这个区域在实现时采用链表的方法。
当调用了 HashSet 的 add 方法存放对象 obj , HashSet 会首先调用 obj 的 hasCode 方法得到该对象的哈希码, HashSet 会使用一个算法把它的哈希码转换成一个数组下标,该下标“标记”了 obj 的位置。如果这个位置上的链表中没有元素,那么就把 obj 对象添加到链表上。如果这个位置上的链表中已经有了元素,则遍历这个链表,调用 obj 的 equals 方法,判断 obj 是否和其中的某个元素重复,如果没有重复的元素,那么就将 obj 添加到链表上;如果有重复的元素,则不会讲 obj 对象存入 HashSet 中。
也就是说,根据哈希表的定义,为了保障相同的对象被放到相同的哈希区域,则必须满足条件:有equals() 返回true=> hashCode() 返回true。 因为先判断的是hashCode的值,换句话说,equals的值为true是hashCode值为true 的充分非必要条件。这样的话,就不会出现两个实际相同的对象,仅仅因为不在同一个哈希区域而被错误的加入到哈希集合中的情况发生了。
2)并且由于链表的缺点在于查询速度慢,所以在我们定义自己的hashCode()和equals()时,为了照顾到哈希表的性能 ,也要遵循“equals返回false时,hashCode也为false”
综合上述1)和2)两点,若hashCode方法和equals不一致则hashCode()和equals()结果没有任何关系,也就是说 equals返回true时,hashcode()也可能是false的,这个与哈希表定义中不允许相同的元素的定义不符合,也不符合哈希表性能优化的需 要。
得出的结论是:建议hashCode和equals方法的判断依据最好是一个,也就是所谓的两个方法兼容。
注意:当一个对象被存储进Hashset中以后,就不能修改这个对象中那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 hashset对象的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去检索hashset集合,也将返回找不到 对象的结果,这会导致无法从hashset集合中单独删除当前对象,从而造成内存泄露。
实例代码如下:
package testhashcode;
/**
* @author gnuhpc
* email: warmbupt@gmail.com
* blog: http://blog.csdn.net/gnuhpc
* @date 2010-1-13
*/
import java.util.*;
class TreeSetTest
{
public static void main(String[] args)
{
HashSet<Student> hs=new HashSet<Student>();
Student st1=new Student(1,"zhao");
Student st2=new Student(2,"qian");
Student st3=new Student(3,"sun");
hs.add(st1);
hs.add(st2);
hs.add(st3);
System.out.println(hs);
st1.num=4; //可以试着注释掉这一行看一看结果
hs.remove(st1);
System.out.println(hs);
}
}
class Student
{
public Student(int num,String name)
{
this.num=num;
this.name=name;
}
public int hashCode()
{
return new Integer(num).hashCode();
}
@Override
public boolean equals(Object st)
{
Student tempStudent= (Student) st;
if (num==tempStudent.num) return true;
else return false;
}
public String toString()
{
return "student "+num+" name:"+name;
}
int num;
String name;
}