首先声明:我用的是JDK1.6,因为才学Java不久,所以写的是很肤浅的东西,高手可以直接略过或鄙视,希望能给那些像我一样的新手带来帮助!
好了,喝口水滋润一下嗓子,争取一口气说完。
HashSet底层维护的实际上是一个Entry类型的数组(数组名为table),而我们知道一般数据存入数组的时候,我们是按先后顺序一个一个存入的,如:先存table[0],再存table[1]……但HashSet的源代码则不是这样存储的,我这里向HashSet中加入的元素是字符串,加入其他类型的元素道理也一样,OK,废话不多说,我们追踪add方法发现它是通过调用HashMap中的put方法实现的,底层代码如下:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key,value, i);
return null;
}
我们具体看addEntry(hash, key, value, i);这一句,这行代码即是把元素key加入到set中,也就是加入到table数组中,跟进去我们发现table的index并不是按照从0开始顺序向后加的,而是由这个方法的第四个参数i来决定的,我们再回去看看i是如何确定的:
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
我们发现i是由要加入的元素的hash code值和table数组的长度决定的,找到这两值我们便可以找到真正的答案了。
咳咳……提提神,我们继续走!
我们先找table的长度,因为HashSet中调用了HashMap中的方法put,所以一定先要生成HashMap的对象才能调用,于是我们在HashSet源代码中发现了下面的代码:
public HashSet() {
map = new HashMap<>();
}
也就是说我们是调用HashMap不带参数的构造方法来生成对象的,于是到HashMap中我们发现:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
这里的DEFAULT_INITIAL_CAPACITY即是我们要找的答案,它是HashMap中定义的常量,值为16,所以table数组的长度为16。
下面继续找要加入的元素的hash code值,因为这里我们加入的元素是String类型,所以我们到String中去找答案,当然不同类型的元素找到的结果不一样,但道理相通。在String中hashCode这个方法的算法是:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
其中的s[i]表示字符串中的第i个字符,n表示字符串的长度,通过这个表达式我们便可以计算出对应的字符串的hash code。
到此我们要找的两个值都找到了,我们发现当我们向HashSet中添加元素时,HashSet底层维护的table数组添加的顺序是由加入的字符串中的具体字符以及字符串的长度决定的,并不是由添加的顺序决定的,而我们添加字符串时则不会事先去算一下这两个值的大小,也没必要,所以对Coder来说HashSet中元素的顺序是未知的,但这并不表示它本身没有顺序,因为一旦HashSet中的元素都确定了,元素的顺序便随上面的两值一起确定了,不管我们怎么运行都不会变,除非我们在运行过程中改变、添加或删除某个值。
我要说的就这么点,只要稍微看下代码就一目了然了,之所以把它写出来,是因为在学习的时候有这个疑惑,希望可以鼓励大家和我自己多看底层代码,不要只会调用而不理解原理。
最后欢迎大家拍砖和交流!