在java集合框架中,ArrayList和HashSet都是比较常用的用来存放对象的集合。但是这两种集合存放元素的方法却有很大的不同。看下面一段代码:
有这么一个类:
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
现在要用他生成几个对象并放到集合中:
public static void main(String[] args) throws Exception{
Collection col=new ArrayList();
Point rp1=new Point(3, 3);
Point rp2=new Point(5, 5);
Point rp3=new Point(3, 3);
col.add(rp1);
col.add(rp2);
col.add(rp3);
col.add(rp1);
System.out.println(col.size());
}
打印结果可以发现,ArrayList中存放了4个元素。ArrayList可以看做一个数组,他自身带有索引,所以他在存放元素的时候并不会判断元素是否相等或相同,而是将对象的引用作为元素的值依次存放在集合中。,现在我们将ArrayList改为HashSet,可以发现,结果变成了3,这说明在存放过程中有一个元素被抛弃了(其实就是最后那句 col.add(rp1)没有执行),为什么会这样呢?
这是因为HashSet在存放元素之前,会先判断一下集合中是否有元素跟要放入的元素相等,如果有的话,那么这个元素就不会被放进去,而不是覆盖到原来的元素,如果想要覆盖掉原来的元素,那么必须首先把原来的元素删除掉,在把这个元素放进去。在上面的代码中,rp1的的引用为同一个引用,所以HashSet判定两次add(rp1)方法添加的是同一个对象,所以只添加一次。而对于rp1和rp3,从设计者的角度来说他们应该是相等的,但是由于两次new对象都给他们分配了空间,那么他们的引用(即地址值)就不相同,HashSet认为这两个对象不相等,所以将他们都添加到集合中。面对这种情况,我们就应该在Point类中覆盖Object类的equals方法,让具有同样x和y的对象相等:
public boolean equals(Object obj) {
if(this==obj)
return true;
if (!(obj instanceof Point)) {
return false;
}
Point p = (Point) obj;
return this.x == p.x && this.y == p.y;
}
打印结果可以发现,HashSet集合中还是三个元素(也有可能是两个)。为什么呢?这就要从集合中查找元素的方式说起了。
如果想查找一个集合中是否包含某个元素对象,采用什么办法呢?通常的办法是逐一取出每个元素与要查找的对象进行比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则停止继续查找并返回肯定的信息,否则返回否定的信息。这时候,问题来了,如果一个集合有很多的元素,譬如有一万个,并且没有包含要查找的对象时,则意味着你的程序需要从该集合中取出一万个元素进行逐一比较才能得出结论。于是有人发明了一种哈希算法来提高从集合中查找元素的效率,这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,然后将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储在哪个区域中,如图:
HashSet就是采用哈希算法存取对象的集合,他内部采用对某个数字n进行取余的方式对哈希码进行分组和划分对象的存储区域。查找时只需要根据对象的hashcode值到对应的区域查找,然后用equals()方法进行比较就可以快速的查找元素。
采用这种方法可以使集合查找元素的时间大大降低,但是同时也会降低存入对象的效率,因为每存入一个对象,都要先计算他的哈希值,然后根据哈希值得到对象在集合中要存放的区域。
上面的例子中,由于我们只重写了equals()方法,没有覆盖hashCode() 方法,由于默认的情况下对象的hashcode是根据内存中的地址算出来的,如果rp1与rp3被分到了不同的区域,那么这两个对象就会都被放到集合中,于是会得到3个元素的结论;如果rp1与rp3处于同一个区域,在调用equals() 方法是就会发现这两个元素相等,于是又会得出2个元素的结论。因此,这里要保证rp1与rp3相等,还需要覆盖hashCode()方法,如下:
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
这样就能保证rp1与rp3只有一个能放到HashSet中。
所以,通常来说,一个类的两个实例对象用equals()方法比较结果相等时,他们的哈希码也必须相等,但反之则不成立,即equals()方法比较结果相等的对象可以有不同的哈希码,或者说哈希码相同的两个对象的equals()方法比较的结果可以不相等。例如,字符串"BB"和"Aa"的equals()方法比较结果肯定不相等,但他们的hashCode()方法返回值却相等。
另外,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值得字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也导致无法从HashSet集合中单独删除当前对象,从而造成内存泄露