HashSet集合为什么是无序的?它是怎么判断元素重复的呢?

首先,我们先来理解它为什么是无序的?

仔细观察以下代码,不难发现,s1,s2,s3,s4是四个完全不同的对象,是因为我们用的是new一个对象,新开辟了一份空间,自然也不是同一个对象。这里提一嘴,可能与题目问的无关。

查看代码

Set set=new HashSet();
        Student s1=new Student("tom",20);
        Student s2=new Student("tom",20);
        Student s3=new Student("tom",20);
        Student s4=new Student("tom",20);
        set.add(s1);
        set.add(s2);
        set.add(s3);
        set.add(s4);

        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }

        System.out.println("-------------");
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());
        System.out.println(s4.hashCode());
        /*
        * 输出如下
        * Student{name = tom, age = 20}
        * Student{name = tom, age = 20}
        * Student{name = tom, age = 20}
        * Student{name = tom, age = 20}
        * -------------
        * 1927950199
        * 868693306
        * 1746572565
        * 989110044
        * */

Object类中有两个方法,分别是

public native int hashCode();
public boolean equals(Object obj) {
        return (this == obj);
    }

当你把元素放入HashSet集合的时候,它会自动调用该元素的这两个方法。(因为Object是所有类的父类,所以自然任何对象都能调用它里面的方法,其中带有native修饰符的是个本地方法,不是用java语言实现的,它返回的是对象的jvm地址,所以new了一个对象之后,它的HashCode值是不同了的。但是如果你是比较字符串的hashCode值得话,因为String中重写了Object中的HashCode()方法,所以由此判断出,如果HashCode值相同,两个对象也不一定相等,举例如下)

先了解下这个,String类中重写的HashCode()方法如下,根据这个代码我们可以总结出一个String类中计算HashCode值的公式s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]。其中s[i]是字符串的第i个字符,它底层把字符串先转换成了一个char类型的数组,^表示求幂。(空字符串的哈希值为零)

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

那这个公式是怎么推导出来的呢?

        /*
        * 我们先试一下Aa是如何计算的
        * 因为h=hash,而在String类中不难发现这样一行代码
        * private int hash; // Default to 0
        * 所以只要字符串不是空的话,它最终就会进入for循环
        * 然后第一次for循环  h=31*0+65;
        * 然后第二次for循环  h=(31*0+65)*31+97;
        * 注意不要把这个式子算出来,否则我们就很难发现规律了
        *
        * 我们再试一下AbB
        * 和上面一样,也是满足条件进入for循环
        * 然后第一次for循环  h=31*0+65
        * 然后第二次for循环  h=(31*0+65)*31+97
        * 然后第三次for循环  h=((31*0+65)*31+97)+66
        * 
        * 走到这里相信大家已经能够推导出公式来了
        * */

有了这个公式s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],我们想找两个哈希值相等但字符串不相等的两个字符串,那还不简单?

        /*
        * 最简单的就是令n=2,于是就有
        * s[0]*31+s[1]=s[0]*31+s[1]
        * 我们把它们换成x,y得到
        * 31x1+y1=31x2+y2
        * 经过化简得
        * x1-x2=1/31(y2-y1)
        * 要使这个等式成立,那么就有(也就是说它们的ASCII码值相差这么多)
        * x1-x2=1
        * y2-y1=31
        * */

满足上面条件的,比如“Aa”和“BB”,“Ba”和“CB”,“Bb”和“CC”等等不计其数。

而HashSet的底层数据结构又是哈希表,它是由数组+链表+红黑树组成的,所以当哈希值相同的时候,也只是意味着它们处于同一个链表或红黑树中,仅仅根据哈希值还不不能分辨出它们是否是同一个对象,所以HashSet中还要执行equals方法,Object中的equals是比较它们的地址值是否相等。(注意,HashSet中的链表虽然处于同一个存储桶,但是它们的地址值是不同的哦,不要搞混淆了,毕竟它是链表)如果哈希值不同,那么它就不会执行equals方法了,因为哈希值不同的一定是不同的对象。

至于第一个问题,为什么是无序的,因为Object中的HashCode()方法返回的是jvm地址值,所以自然是无序的。如果是重写了HashCode()方法的话,那么它会根据值大小来排列,但是不一定先加入集合的对象的哈希值就小,因为这是根据对象里面的数据算出来的,所以大小虽然是固定的,但是没算出来之前,我们不能判断出谁大谁小。所以自然也是无序的。

看到这里,第二个问题应该也能够明白了。

 

Java小白,如果不对或疑问,可评论或私信指正。

posted @ 2022-04-16 09:54  朱在春  阅读(232)  评论(0编辑  收藏  举报