HashSet是Set集合最典型的的实现,也是最常用的,HashSet利用Hash算法来存储集合中的元素,因此具有很好的存储和查找的性能。查看源码发现HashSet在底层实现时时转化为HashMap实现的
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
HashSet具有以下特点:
- 无序性,集合最后的顺序和添加的顺序无关
- 不是同步的,如果在多线程中需要用到HashSet就必须通过代码在保证同步
- 集合元素可以为null
- 不可重复,这里的不可重复是指HashSet中无法存储两个相同的元素,那么何为相同元素呢?就是用equals()方法判断像个元素相同并且两个元素的Hash码一样,下面通过一个例子具体说明HashSet判断集合元素是否相同的标准。
public class HashSetDemo1 {
public static void main(String[] args) {
HashSet hs = new HashSet();
//将两个A对象添加到集合中
hs.add(new A());
hs.add(new A());
//将两个B对象添加到集合中
hs.add(new B());
hs.add(new B());
//将两个C对象添加到集合中
hs.add(new C());
hs.add(new C());
System.out.println(hs);
//输出结果为
//[collection.B@1, collection.B@1, collection.A@33909752, collection.C@2, collection.A@55f96302]
}
}
// 重写类A的equals方法永远返回true,说明每个A的实例用equals方法判断都是相等的
class A{
@Override
public boolean equals(Object obj) {
return true;
}
}
//类B重写了hashCode方法,返回值为1,说明B的所有实例Hash码值永远为1
class B{
@Override
public int hashCode() {
return 1;
}
}
//类c两个方法都重写了
class C{
@Override
public int hashCode() {
return 2;
}
@Override
public boolean equals(Object obj) {
return true;
}
}
从上面可以发现就两个C类对象只添加了一个。总结一下HashSet的判断元素重复的步骤1)先根据hash算法判断出数组对应的下标,2)调用equals方法,如果返回true说明两个元素相等,则不会添加3)如果不相等就会发生hash碰撞,在链表或者红黑树结构里添加元素。所以如果要使用HashSet和HashMap集合,必须保证元素重写了HashCode和equals方法,而且要保证在equals相等的同时HashCode的返回值一样。
如何重写HashCode方法
接下来主要是讨论一下,我们自己定类是该如何重写HashCode方法,首先我们必须要保证以下规则
1)在程序运行时,同一个对象多次调用hashCode方法时必须返回同一个值
2)当两个对象通过equals()方法比较返回值为true时,这两个对象的hashCode()方法返回值必须一致
3)对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值
重写hashCode()方法的一半步骤:
1)把对象中每个有意义的实例变量(即参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值,计算方式为下表
实例变量类型 | 计算方式 |
boolean | hashCode=(f?0:1) |
整型类型(byte、short、char、int) | hashCode=(int)f |
long | hashCode=(int)(f^(f>>>32)) |
float | hashCode=Float.floatToIntBits(f) |
double | long l=Double.doubleToLongBits(f); hashCode=(int)(l^(l>>>32)) |
引用类型 | hashCode=f.hashCode() |
2)用第一步计算出来的值组合计算出一个hashCode值返回。下面的例子中f1和f2都是对象的实例变量,f1变量时有意义的那个。
return f1.hashCode*19+(int)f2.hashCode
注意点
向HashSet中添加可辨对象时,必须十分小心,如果修改HashSet集合中的对象,有时可能导致该对象与集合中的其他元素相等,从而导致HashSet无法准确访问该对象,看下面的例子
public class HashSetDemo {
public static void main(String[] args) {
Set<R> set=new HashSet<>();
set.add(new R(5));
set.add(new R(-3));
set.add(new R(9));
set.add(new R(-2));
//输出结果为[R[count:-2], R[count:-3], R[count:5], R[count:9]]
System.out.println(set);
//将-3赋值给第一个元素的count变量
Iterator<R> it= set.iterator();
R r = it.next();
r.count=-3;
//[R[count:-3], R[count:-3], R[count:5], R[count:9]]可以看到元素发生改变
System.out.println(set);
//删除一个count值为-3的对象
set.remove(new R(-3));
//[R[count:-3], R[count:5], R[count:9]]我们可以看出确实删除了一个元素,但是到底是删除的第一个还是第二个?
System.out.println(set);
//先来判断以下这个set中是够还含有count为-3的元素了,输出为false。
System.out.println(set.contains(new R(-3)));
//在看一下是否含有count为-2的元素,输出为false
System.out.println(set.contains(new R(-2)));
}
}
/**
* 定义一个R类,用来封装一个int类型的变量
* @author huangyifan
*
*/
class R{
//为了方便修改对象的count变量这里就不用private修饰了
int count;
public R(int count) {
this.count = count;
}
//重写equals方法
@Override
public boolean equals(Object obj) {
if(obj == null) return false;
if(this == obj) return true;
if(R.class == obj.getClass()) {
R a = (R)obj;
return a.count == this.count;
}
return false;
}
//重写hashCode方法
@Override
public int hashCode() {
return this.count;
}
//重写toString方法方便我们查看控制台的结果
@Override
public String toString() {
return "R[count:"+count+"]";
}
}
下面我们来分析底层的结构,更好的说明修改了HashSet元素后,无法正常访问元素的原因,首先系统会根据hash算法确定数组下标,从而确定元素存放的位置。运行完添加元素操作后,内存结构如下图
然后将第一个元素的count值该为-3后
这时候执行
set.remove(new R(-3));
由于系统是根据hash算法确定数组下标的然后在通过equals()方法比较两个对象是否相同,所以删除的是第二个元素。很好理解,因为我们重写hashCode()时只用到了count这一个变量,所以系统经过hash算法后找的位置应该和当初添加元素找到的位置一样。该位置就是下标为1的位置。
接着运行
System.out.println(set.contains(new R(-3)));
输出结果为false,和删除对象的原理一样,系统此时找到的位置是下表为1的位置,此时这个位置上没有元素,所以返回false。
最后运行
System.out.println(set.contains(new R(-2)));
是否包含count为-2的元素,很显然更具count=-2,系统会找到下标为0的位置,然后在通过equals()方法进行比较,发现两个对象的count值不一样,所以返回false。