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小白,如果不对或疑问,可评论或私信指正。