[java][集合]HashSet详解

本文深入探讨了HashSet的工作原理,包括其内部实现机制、元素添加与查找的过程,并通过具体示例展示了如何正确重写equals和hashCode方法以确保集合的正确行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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具有以下特点

  1. 无序性,集合最后的顺序和添加的顺序无关
  2. 不是同步的,如果在多线程中需要用到HashSet就必须通过代码在保证同步
  3. 集合元素可以为null
  4. 不可重复,这里的不可重复是指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值,计算方式为下表

实例变量类型计算方式
booleanhashCode=(f?0:1)
整型类型(byte、short、char、int)hashCode=(int)f
longhashCode=(int)(f^(f>>>32))
floathashCode=Float.floatToIntBits(f)
doublelong 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。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值