分析HashSet集合存储自定义类型对象的存储过程

本文详细分析了HashSet如何存储自定义类型对象,解释了为何需要重写hashCode和equals方法。通过实例展示了不重写时,HashSet会基于对象地址判断,而重写后则依据对象内容判断,确保了id相同的Student对象不被重复添加。

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

首先创建一个学生类

class Student{
	
	String id;

	public Student(String id) {		
		this.id = id;
	}
}

然后向HashSet集合存储学生类型的对象:

HashSet<Student>set = new HashSet<>();
set.add(new Student("1"));
set.add(new Student("1"));

分析底层代码:

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

根据上一篇博客:《HashSet中的add方法(对集合底层的分析)》https://blog.youkuaiyun.com/qq_40831381/article/details/90119935   由此可知:

执行第一行代码过程---->调用HashSet无参构造方法---->hashCode无参构造方法----->可知HashMap中table变量为null。

执行第二行代码过程---->tab=resize();方法为table变量赋值---->该值即为该方法的返回值---->tab局部变量与table全局变量指向同一个数组,数组长度是16,向set集合中成功添加第一条数据

执行第三行代码过程---->由于new Student("1")和new Student("1")对象的地址不同,所有依旧能向set集合中成功添加第二条数据

 重写hashCode

     在上面写的代码中,由于Student类中并没有重写hashCode()方法,所以在调用hash(Object key)时,进而调用的是object中hashCode方法:(return super.hashCode();)比较的是传入对象的地址值,又测试代码中add()方法传入的两个对象的地址值不相同,故hash值不同。由上一篇博客的分析可知,在计算机中,这两个对象都可以添加进HashSet集合中。但是我们人为的希望比较的是Student类中的id是否相同,id相同视为同一个学生对象,实现不能重复添加id相同的Student对象的功能,即不需要比较地址是否一致,只要是值一致即可。这时可以通过在自定义的Student类中重写hashCode()方法来实现。代码如下:

@Override
public int hashCode() {		
		
	return id.hashCode();
	}

这时就会调用我们重写的这个该hashCode()方法实质上是在调用id的hashCode方法,而id是String类型的,故实际上调用的是String类中的hashCode()方法,比较内容是否相同,上述代码中创建的Student对象中所传入的id内容相同,那么hash值相同,而不再是调用object中的hashCode方法。保证了(p = tab[i = (n - 1) & hash])不为空,执行else分支

具体执行过程:

第二行代码set.add(new Student(“1”))的执行过程

1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句代码块,tab=resize();方法为table变量赋值,此处的table是在HashMap类中定义的成员变量,由于在声明时未显示初始化,HashMap类中的所有构造方法当中也没有对table变量赋值,故该值默认为null。故条件成立,执行if代码块中的n = (tab = resize()).length语句,resize()方法执行完后会返回一个默认初始长度为16的数组,故tab和table都指向该数组。
2.程序继续执行if ((p = tab[i = (n - 1) & hash]) == null)语句,此处的i值是根据所传key的hash值对tab数组空间的一个映射,也就是(n - 1) & hash为数组下标。此时tab[i]中没有数据,if语句成立,执行tab[i] = newNode(hash, key, value, null),创建一个Node对象(封装id="1"的Student对象)并存到tab[i]中,也就是说此时tab(table)指向的数组中已经存储了一个id="1"的Student对象。
3.程序执行了if后 跳过else语句执行下面代码,返回null。即putVal方法返回null,put方法也返回null,add方法返回true,添加成功。


   第三行代码set.add(new Student(“1”))的执行过程

1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句,由于此处的tab(table)指向的数组中已经存储了一个new Student("1")的对象,不为空,故条件不成立,不再继续执行if代码块中的内容。
2.程序继续执行if ((p = tab[i = (n - 1) & hash]) == null)语句,由之前的分析可知此处的hash值与执行第二行代码时的hash值相同,故i值不变,即tab[i]中存储的是id="1"的Student对象,变量p指向tab[i],保证了p = tab[i = (n - 1) & hash])不为空,故条件不成立,跳过if代码块,执行else代码块中的内容。
3.程序继续执行 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))语句,p.hash == hash语句实质是在比较第一次传入的Student对象的hash值和本次传入的Student对象的hash值,由之前的分析可知为true,但是(k = p.key) == key中,k=p. key即k和p中存储的都是第一次传入的Student对象的地址,而key是本次传入的Student对象的地址,显然不同,故而执行key != null && key.equals(k),key!=null成立执行key.equals(k),由于key是自定义的Student类,并没有重写Object类中的equals方法,所以此时调用的仍然是Object类中的equals方法,比较地址值是否相同,结果为false,而如果要实现人为的希望比较的是Student类中的id是否相同的功能,此处结果必须为true,所以需要在Student类中重写equals方法。

 重写equals方法

​@Override
public boolean equals(Object obj) {
		
	Student student = (Student)obj;
	return id.equals(student.id);
	}​

重写equals方法之后,此处实际上是调用Student类中的重写后的equals方法,而该方法实际是调用id的equals方法,而id是String类型的,故实际上调用的是String类中的equals()方法,比较内容是否相同,显然为true。条件成立,执行e = p。
此时,程序执行if (e != null) {…}代码块中的内容,返回一个不为null的值,即putVal方法返回值不为null,put方法返回值也不为null,add方法返回false,添加失败。实现了人为的希望比较的是Student类中的id是否相同的功能,即不能重复添加id相同的Student对象的功能。

当泛型为Object的时候传入其他的非Student类的时候有可能会出现异常

为了防止存入不同的对象,需要使用 instanceof,代码如下:

@Override
public boolean equals(Object obj) {
	//return super.equals(obj);
	if (obj instanceof Student) {
		Student student = (Student)obj;
		return id.equals(student.id);
	}
	return false;		
	}	

总结:

 1.hashCode和equals的执行顺序:先执行hashCode方法,在hashCode相等的情况下,再执行equals方法

 2.重写equals方式时,注意使用instanceof

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值