HashSet添加重复自定义类的逻辑梳理

本文详细解析了Java中对象地址、属性与hashcode的关系,讨论了根据地址和属性生成hashcode对HashSet添加元素的影响,并提供了针对自定义类和包装类的建议。通过实例测试,强调了如何确保唯一性和重复元素的避免。

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

先说一下结论:包装类直接添进去,能保证不会重复添加。如果是自定义类,重写以属性内容生成hashcode的方法,别重复添加修改属性后的同地址对象。
下面是对源码分析和证明部分,有误望指出!

一、什么是hashcode?

百度解释
在这里插入图片描述

1.可以简单理解为将任意长数据经哈希算法变为固定长度的一串数字。
2.不同数据生成的hashcode有可能相同,我们称之为哈希碰撞。

二、对象地址,属性,hashcode之间的关系梳理

1.先上一张总关系图,可以边打开图放一边,一边看我文字描述。
在这里插入图片描述

2.在java中生成对象的hashcode有两种方式
1、只根据地址生成hashcode

Cat类中重写该方法

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

测试同属性不同地址下hashcode的生成情况:

    @Test
    /**
     * 测试只根据地址生成hashcode是否成立
     *
     */
    public void test1(){
        Cat mm = new Cat("喵喵");
        System.out.println(mm.hashCode());
        Cat mm1=new Cat("喵喵");
        System.out.println(mm1.hashCode());

    }

结果为:
1859039536
278934944
结论:说明这种方法生成的hashcode只与地址有关与属性无关。同一地址的对象的属性值可以随便更换(看总关系图)。
【注意】这里hashcode定长是指二进制定长32,即4字节,可用整型来存储
2.只根据属性生成hashcode

Cat类重写该方法
    @Override
    public int hashCode() {
        return Objects.hash(getName());//只根据属性
    }

测试同地址不同属性情况下的hashcode:

 @Test
    /**
     * 测试只根据属性生成hashcode是否成立
     *
     */
    public void test2(){
        Cat m1 = new Cat("喵喵");
        System.out.println(m1.hashCode());
        m1.setName("喵呜");
        System.out.println(m1.hashCode());
    }

结果:
702143
701798
结论:说明只根据属性生成hashcode的方法也成立。同一个属性可以被多个不同地址的对象引用(看总关系图)。

三、向HashSet添加元素时的关键源码分析

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //表示如果是第一次添加,那就先扩容,这里默认初始化为16个Node长度的		   数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            //根据hash值运算得到待添加key值该放到数组哪里去,如果运算得到待添入位置出没有已存在的Node,那就新建个Node,Node里存好key,把Node存进该Node数组的该位置处
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //如果计算出的数组待添入位置处有Node了,则进一步比较判断是否加入
            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;
    }

四、添加不同元素可能出现的情况

回到总关系图
在这里插入图片描述
情况1:如果已存在null值那么经计算得到的hashcode肯定一样,再计算出来的在数组中的位置肯定也一样,那么必然要经过这道考验if (p.hash != hash || ((k = p.key) != key && !(key != null && key.equals(k))));数组该位置处子链上可能除了null还有其他元素,跟其它元素比较时如果p.hash!=hash为true则比较子链下一个,如果运算出来是false,则发生了hash碰撞,那么看后边p.key!=key也为true,!(key != null && key.equals(k)))也为true,那么得出结论,hash值不同就继续检索,发生hash碰撞就加上后边判断条件,按这种判断条件,如果内容不是null,即使hash碰撞也要继续往下检测,直到发生hash碰撞并后边判断null==null才不让加。
代码验证:

    @Test
    /**
     * 在地址生成hashcode情况下
     * 地址同,内容改,看是否会重复添加?
     * [Dog{name='bb'}]不会重复,只会改,即保证不会重复添加同地址对象
     */
    public void test2(){
        HashSet hashSet = new HashSet();
        Dog aa = new Dog("aa");
        hashSet.add(aa);
        aa.setName("bb");
        hashSet.add(aa);
        System.out.println(hashSet);
    }

在这里插入图片描述

    @Test
    /**
     * 在地址生成的hashcode情况下
     * 测试地址不同,内容相同是否可添加
     *[Dog{name='aa'}, Dog{name='aa'}]可添加
     */
    public void test3(){
        HashSet hashSet = new HashSet();
        Dog aa = new Dog("aa");
        Dog a2= new Dog("aa");
        hashSet.add(aa);
        hashSet.add(a2);
        System.out.println(hashSet);

    }```
![在这里插入图片描述](https://img-blog.csdnimg.cn/08b2aa19b68548ebbd04ec0cc846a556.png)

情况2:如果计算出的位置处啥也没有,那肯定直接添,如果有但又不是已存在其它元素则类似情况1,因为没有null,所以一直往下成功检索,检索到子链尾并添加。
【注意】java1.8之后null的hashcode固定为0,之前null都没有hashcode不允许加入到hashSet或hashMap。总节之,null就是单纯的不可重复添加。

情况3456:这又得看是重写了以地址生成hashcode方法还是以属性生成hashcode方法。
	1:如果以地址生成hashcode,如果里边已经存着一个同地址的,当没检索到存着同地址的那个Node头上时,一般来说没有hash碰撞,p.hash!=hash为true这样就直接获得继续检索的资格,如果发生hash碰撞,p.hash!=hash为false那么继续进行判断,p.key!=key为true,继续判断!(key != null && key.equals(k)),这里如果属性相同则为false,那么整个判断为false,那么就失去检索资格,被判定为重复。刚好检索到存着同地址的那个Node时,那么hash值	肯定一样,p.hash!=hash为false,p.key!=key为false这时就不用看了,肯定在这就停了,它失去了添加的资格。
【总结】这种以地址生成hashcode,在添加时,能保证同地址的绝对不添加,但修改地址上的内容可以时hashSet里的内容同步修改。不同地址的放进去如果一直没有hash碰撞,则直接放进去,会导致属性重复的放进去。如果hash碰撞还能够检测到属性内容重复,直接劝退。所以用地址生成hashcode只能保证存放的对象地址肯定不一样,其它就不能保证了。
	2.如果以属性内容生成hashcode,如果里边已经存着一个同属性内容的,当没检索到存着同属性的那个Node头上时,一般来说没有hash碰撞,p.hash!=hash为true这样就直接获得继续检索的资格,如果发生hash碰撞,p.hash!=hash为false,继续判断p.key!=key为true,在属性地址存在于同一对象地址时,为false,那么停止检索,失去添加资格,如果属性存在于其它地址对象,为true,那么继续进行判断,!(key != null && key.equals(k))true,继续检索。刚好检索到存着同属性的那个Node时,那么hash值	肯定一样,p.hash!=hash为false,p.key!=key为false这时就不用看了,肯定在这就停了,它失去了添加的资格,如果为ture,!(key != null && key.equals(k))false,当场拒绝添加。
	【总结】这种以属性内容生成hashcode,在添加时仅保证同属性内容的绝对不会添加,但不同属性同地址在没有hash碰撞时仍能添加,这就导致hashSet内会存在两个重复对象重复属性,不同属性同地址在发生hash碰撞就可以阻止同地址的重复添加。
	代码验证:
```java
    @Test
    /**
     * 在属性生成hashcode情况下
     * 地址同,内容改,看是否会重复添加?
     * [Dog{name='bb'}, Dog{name='bb'}]会重复
     */
    public void test4(){
        HashSet hashSet = new HashSet();
        Dog aa = new Dog("aa");
        hashSet.add(aa);
        aa.setName("bb");
        hashSet.add(aa);
        System.out.println(hashSet);
    }

在这里插入图片描述

    @Test
    /**
     * 在属性生成的hashcode情况下
     * 测试地址不同,内容相同是否可添加
     *可添加
     */
    public void test5(){
        HashSet hashSet = new HashSet();
        Dog aa = new Dog("aa");
        Dog a2= new Dog("aa");
        hashSet.add(aa);
        hashSet.add(a2);
        System.out.println(hashSet);

    }```
![在这里插入图片描述](https://img-blog.csdnimg.cn/7deabd37d88543ca821c5b4d4f148253.png)

【大总结】一般我们往hashSet里添对象都不会添重复地址的对象,这就减少了重复添地址的概率,这在以地址生成hashcode情况下,还有添加重复属性不同对象的可能。但如果以属性生成hashcode情况下,地址重复概率低,接着保证属性不会重复添加,就基本保证了地址不同且属性不同的最终要求。之所以造成这些可能的不符合HashSet不添加重复元素要求的情况,就是因为属性不是final修饰,可以任意更换。而一般我们存包装类啥的数据,人家里边属性本身就是final修饰,平时也就用基本数据类型自动转包装类型而规避了这些特殊情况。如果要自定义类,然后存里头,就可能遇到这些奇奇怪怪的事情。经这次测试研究,得出结论:基本数据类型的包装类百分百保证重复的添不进。如果想添自定义类,属性给设成final,奇葩操作造成的混乱可避免掉。如果不能设成final,自个儿注意别重复添同一地址对象,想修改对象内容,迭代遍历找出来,再修改。直接改完再添会出乱子。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值