15capacity<<=1;
16
17this.loadFactor=loadFactor;
18threshold=(int)(capacity*loadFactor);
19table=newEntry[capacity];
20init();
21}
22
23publicHashMap(intinitialCapacity){
24this(initialCapacity,DEFAULT_LOAD_FACTOR);
25}
26
27publicHashMap(){
28this.loadFactor=DEFAULT_LOAD_FACTOR;
29threshold=(int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR);
30table=newEntry[DEFAULT_INITIAL_CAPACITY];
31init();
32}
我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。
默认初始容量为16,默认加载因子为0.75。
我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂,至于为什么要把容量设置为2的n次幂,我们等下再看。
重点分析下HashMap中用的最多的两个方法put和get
3、存储数据
下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:
publicVput(Kkey,Vvalue){
//若“key为null”,则将该键值对添加到table[0]中。
if(key==null)
returnputForNullKey(value);
//若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
inthash=hash(key.hashCode());
//搜索指定hash值在对应table中的索引
inti=indexFor(hash,table.length);
//循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。
然后退出!
for(Entrye=table[i];e!
=null;e=e.next){
Objectk;
if(e.hash==hash&&((k=e.key)==key||key.equals(k))){//如果key相同则覆盖并返回旧值
VoldValue=e.value;
e.value=value;
e.recordAccess(this);
returnoldValue;
}
}
//修改次数+1
modCount++;
//将key-value添加到table[i]处
addEntry(hash,key,value,i);
returnnull;
}
上面程序中用到了一个重要的内部接口:
Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。
从上面程序中可以看出:
当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。
这也说明了前面的结论:
我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
我们慢慢的来分析这个函数,第2和3行的作用就是处理key值为null的情况,我们看看putForNullKey(value)方法:
1privateVputForNullKey(Vvalue){
2for(Entrye=table[0];e!
=null;e=e.next){
3if(e.key==null){//如果有key为null的对象存在,则覆盖掉
4VoldValue=e.value;
5e.value=value;
6e.recordAccess(this);
7returnoldValue;
8}
9}
10modCount++;
11addEntry(0,null,value,0);//如果键为null的话,则hash值为0
12returnnull;
13}
注意:
如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。
即table[0]
我们再回去看看put方法中第4行,它是通过key的hashCode值计算hash码,下面是计算hash码的函数:
1//计算hash值的方法通过键的hashCode来计算
2staticinthash(inth){
3//ThisfunctionensuresthathashCodesthatdifferonlyby
4//constantmultiplesateachbitpositionhaveabounded
5//numberofcollisions(approximately8atdefaultloadfactor).
6h^=(h>>>20)^(h>>>12);
7returnh^(h>>>7)^(h>>>4);
8}
得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:
1staticintindexFor(inth,intlength){//根据hash值和数组长度算出索引值
2returnh&(length-1);//这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出
3}
这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。
接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。
首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h&(table.length-1)hashtable.length-1
8&(15-1):
0100&1110=0100
9&(15-1):
0101&1110=0100
-----------------------------------------------------------------------------------------------------------------------
8&(16-1):
0100&1111=0100
9&(16-1):
0101&1111=0101
从上面的例子中可以看出:
当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
根据上面put方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该key的hashCode()返回值决定该Entry的存储位置:
如果两个Entry的key的hashCode()返回值相同,那它们的存储位置相同。
如果这两个Entry的key通过equals比较返回true,新添加Entry的value将覆盖集合中原有Entry的value,但key不会覆盖。
如果这两个Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry链,而且新添加的Entry位于Entry链的头部——具体说明继续看addEntry()方法的说明。
1voidaddEntry(inthash,Kkey,Vvalue,intbucketIndex){
2Entrye=table[bucketIndex];//如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
3table[bucketIndex]=newEntry<>(hash,key,value,e);
4if(size++>=threshold)//如果大于临界值就扩容
5resize(2*table.length);//以2的倍数扩容
6}
参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。
第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。
4、调整大小
resize()方法如下:
重新调整HashMap的大小,newCapacity是调整后的单位
1voidresize(intnewCapacity){
2Entry[]oldTable=table;
3intoldCapacity=oldTable.length;
4if(oldCapacity==MAXIMUM_CAPACITY){
5threshold=Integer.MAX_VALUE;
6return;
7}
8
9Entry[]newTable=newEntry[newCapacity];
10transfer(newTable);//用来将原先table的元素全部移到newTable里面
11table=newTable;//再将newTable赋值给table
12threshold=(int)(newCapacity*loadFactor);//重新计算临界值
13}
新建了一个HashMap的底层数组,上面代码中第10行为调用transfer方法,将HashMap的全部元素添加到新的HashMap中,并重新计算元素在新的数组中的索引位置
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。
所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:
原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?
当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。
也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提