为什么HashMap初始大小为16,为什么加载因子大小为0.75,这两个值的选取有什么特点?

时间:2021-02-26 19:11:11

先看HashMap的定义:

public class HashMap<K,V>extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

HashMap是AbstractMap的子类,实现了Map接口。

HashMap()

Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).

HashMap(int initialCapacity)

Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75).

HashMap(int initialCapacity, float loadFactor)

Constructs an empty HashMap with the specified initial capacity and load factor.

HashMap(Map<? extends K,? extends V> m)

Constructs a new HashMap with the same mappings as the specified Map.

总共给出了4中构造方法。

1.HashMap()   不带参数,默认初始化大小为16,加载因子为0.75;

2.HashMap(int initialCapacity)  指定初始化大小;

3.HashMap(int initialCapacity ,float loadFactor)指定初始化大小和加载因子大小;

4.HashMap(Map<? extends K,? extends V> m) 用现有的一个map来构造HashMap。

先分析一下初始化代码

public HashMap(int initialCapacity,float loadFactor){  
   if(initialCapacity<0) //初始化大小小于0,抛出异常  
       throw new IllegalArgumentException("Illegal initial capacity: "  
       +initialCapacity);  
   if(initialCapacity>MAXIMUM_CAPACITY)//初始大小最大为默认最大值  
       initialCapacity=MAXIMUM_CAPACITY;  
   if(loadFactor <=0|| Float.isNaN(loadFactor)) //加载因子要在0到1之间  
       throw new IllegalArgumentException("Illegal load factor: "  
       +loadFactor);  
   this.loadFactor =loadFactor;  
   this.threshold=tableSizeFor(initialCapacity);  
   //threshold是根据当前的初始化大小和加载因子算出来的边界大小,  
   //当桶中的键值对超过这个大小就进行扩容  
}

 

threshold=initialCapacity*loadFactor,桶中的键值对超过这个界限就把桶的容量变大。

 

if ((tab = table) == null || (n = tab.length) == 0)  
   n = (tab = resize()).length;

 

这是第一次向桶中放入元素时(put方法)的一次扩容,因为初始化时并没有指定桶的大小

每次调用put方法后,都会进行验证,判断是否需要扩容

if (++size > threshold)  
           resize();

 

在看一下扩容的代码

 

Node<K,V>[] oldTab=table;    
int oldCap=(oldTab==null)?0:oldTab.length;// 第一次扩容时旧的桶大小为0  
int oldThr=threshold; //桶中键值对数量的边界大小。  
int newCap,newThr=0; //新的桶大小和边界大小。  
if(oldCap>0){ //这是后来扩容时进入的,第一次指定桶大小时不会进入这  
   if(oldCap>= MAXIMUM_CAPACITY){//最大只能这么大了  
       threshold=Integer.MAX_VALUE;  
       return oldTab;  
   }  
   else if((newCap=oldCap<<1)<MAXIMUM_CAPACITY&&  
           oldCap>=DEFALT_INITAL_CAPACITY)  
       newThr=oldThr<<1;   //如果旧的桶大小扩大一倍还没有超过最大值,  
                            //就把旧的桶大小和新的边界都乘2  
}  
else if(oldThr>0)    
   newCap=oldThr; //第一次扩容时会进入这里初始化桶的大小  
if(newThr==0){  //根据初始化桶数组大小和加载因子已经是否越界来设置新边界  
   float ft=(float)newCap*loadFactor;  
   newThr=(newCap<MAXIMUM_CAPACITY && ft<(float)MAXIMUM_CAPACITY?  
           (int)ft:Integer.MAX_VALUE);  
}

下面我们来讨论一个问题。

为什么默认初始化桶数组大小为16,为什么加载因子的大小为0.75,这两个值的选取有什么特点。

通过看上面的代码我们可以知道这两个值主要影响的threshold的大小,这个值的数值是当前桶数组需不需要扩容的边界大小,

我们都知道桶数组如果扩容,会申请内存空间,然后把原桶中的元素复制进新的桶数组中,这是一个比较耗时的过程。既然这样,那为何不把这两个值都设置大一些呢,threshold是两个数的乘积,设置大一些就不那么容易会进行扩容了啊。

原因是这样的,如果桶初始化桶数组设置太大,就会浪费内存空间,16是一个折中的大小,既不会像1,2,3那样放几个元素就扩容,也不会像几千几万那样可以只会利用一点点空间从而造成大量的浪费。

加载因子设置为0.75而不是1,是因为设置过大,桶中键值对碰撞的几率就会越大,同一个桶位置可能会存放好几个value值,这样就会增加搜索的时间,性能下降,设置过小也不合适,如果是0.1,那么10个桶,threshold为1,你放两个键值对就要扩容,太浪费空间了。