1. 为什么要用哈希表
因为哈希表的插入,删除,查找等算法都是常量级的,速度要远远大于基于数组的集合。因此,如果频繁的执行插入,删除,查找操作,应当优先考虑哈希表。
2. 使用哈希表时应主要的问题
2.1如果重写了GetHashCode,也必须重写Equals。反之亦然。重写Equals时应注意,如果两个对象的值相等,那么他们的哈希值也应相等,譬如String对象。但是两个具有不同哈希值的对象,Equals方法可以返回false,为了哈希处理速度,最好这样做。
举一个具体的例子,请看如下代码:
publicclasshassetTest {
publicstaticvoid main(String[] args) {
HashSet<Point>pointset=new HashSet<Point>();
Pointp1=newPoint(3,3);
Pointp2=newPoint(5,5);
Pointp3=newPoint(3,3);
pointset.Add(p1);
pointset.Add(p2);
pointset.Add(p3);
Console.WriteLine(pointset.Count;//这里输出3,显然不是想要的结果。
}
class Point{
publicintx;
publicinty;
public Point(int x,int y){
this.x=x;
this.y=y;
}
}
上述代码中,首先创建一个HashSet哈希表,然后向这个表中添加一些Point对象。这里创建了三个Point对象,然后将他们添加到HashSet中,这时哈希表中有三个元素,显然这不是我们想要的结果,因为p1和p2两个对象具有相同的值,二者只能有一个被添加到哈希表中。
这时候通过添加GetHashCode和Equals方法,就能够达到我们想要的结果:哈希表中不具有重复的值。如下:
public int GetHashCode() {
int prime = 31;
int result = 1;
result= prime * result + x;
result= prime * result + y;
return result;
}
public bool Equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
Point other = (Point) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
returntrue;
}
如果我们删除掉hashcode或者删除掉Equals方法,结果是怎么的呢?答案是哈希表中可能不具有唯一元素。
因为HashSet的add方法内部不仅调用了GetHashCode来检查对象的哈希值,他还调用了equals方法来检查两个对象是否相等,只有当两者同时满足的情况下,新元素才不能被添加到集合中。
简单的说,在一个HashSet中添加一个新对象时,首先会获取这个新对象的哈希值。这个哈希值指出了这个对象应该存储在哪一个槽中。然后根据这个哈希值遍历这个槽,从而找出那个对象。找出这个对象之后,还会进一步的调用这个槽中的对象以及要添加的对象的Equals方法,只有当equals方法返回true时,才说明这个新对象和哈希表中现有的的这个对象“同一个对象”。如果在重写GetHashCode的同时,忘记了重写equals方法,这是调用的就是基类的Equals,如果这个对象没有显示基础任何类,那么调用的就是Object的Equals方法。Object方法比较的是两个对象的引用,而不是比较对象的内容。因此,当添加的是一个新对象时,并且这个新对象与现有的一个对象具有相同的值,那么就会出问题。
因此,如果重写了hashcode或者equals中的一个,必须还要重写另一个。
2.2哈希算法至少应使用一个实例字段,这主要是为了使算法具有良好的随机分布,是哈希表获得最佳的性能。
2.3理想情况下,参与哈希算法的实例字段应该是不可变的,也就是说,这个实例子弹在实例化完对象之后,就不能再改变。
如果向哈希表中添加了一个元素,用户不小心更改了对象的值(参与哈希算法的那个实例字段),用户可能不知道他更改的是参与哈希算法的那个实例字段,在大多数时候,用户甚至都不知道他更改了这个对象。
一旦用户更改了这个参与哈希算法的实例字段的值,这对象就有了一个新的哈希值。当他再次使用这个更改后的对象去查找哈希表时,这个新的哈希值就会与旧的哈希值不匹配,从而导致一些不易察觉的问题。譬如当要删除这个对象时,我们以为删除了,事实上却没有删除,从而导致可能的内存泄露问题。为了避免这种情况发生,在删除的时候要测试一下是否删除成功,如下:
Booleanisremoved= pointset.remove(p1);
if(isremoved){
//.....
}else{
//。。。。
}
内存泄露还不是最重要的问题,最重要的是结果会出乎我们的意料,即使调试也很难排查出这种错误。
如果,我们能够保证参与哈希算法的实例字段是个常量,那么就可以防止上述所发生的种种问题。因为常量在对象创建完之后就再也不能更改,如果我们的代码无意中更改了这个常量值,编译器会提示错误的。
最后再强调一次,向哈希表中添加一个元素之后,不能再修改参与哈希算法的实例字段,但是可以修改其他实例字段。因为这些字段没有参加哈希运算,所以你怎么修改它对于哈希表来说都没有什么影响。如果我们向point类中新添加一个字段z,但不要修改GetHashCode以及Equals方法,那么我们修改这个z值,对于哈希表来说没有什么影响的。