一、HashMap的构造方法
HashMap默认大小16,加载因子是0.75。但new HashMap()的时候,默认是没有初始化容量的,它的table是一个长度为0的数组 (JDK8是null数组)。
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity;//JDK7的写法。 this.threshold = tableSizeFor(initialCapacity);//JDK8的写法 init();//jdk7此方法里面是空的。jdk8中就没有此行。 }
二、HashMap增加元素
当我们第一次put数据的时候,才会初始化table。
以下是JDK7的put源码。在构造方法执行完成后,table为长度为0的数组。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
public Vput(Kkey,V value) {
if (table== EMPTY_TABLE) {
inflateTable(threshold);
}
而Java8中的
transient Node<K,V>[]table;//默认为null,这里的类型是Node,是Entry的子类
public Vput(Kkey,V value) {
return putVal(hash(key),key,value, false, true);
}
final V putVal(inthash,K key,V value, booleanonlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;Node<K,V> p; int n,i;
if ((tab =table) ==null || (n = tab.length) ==0)
n = (tab = resize()).length; //这里是第一次初始化空间
if ((p = tab[i = (n -1) & hash]) ==null)
tab[i] = newNode(hash,key,value, null);//找到没占用的坑位
else {
Node<K,V> e;K k;
if (p.hash== hash &&
((k = p.key) == key || (key !=null && key.equals(k))))
e = p;
else if (pinstanceof TreeNode)//解决JDK7中变成链表的问题
e = ((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value);
else {
for(intbinCount = 0; ;++binCount) {//遍历坑位深度
if((e = p.next) ==null) {
p.next= newNode(hash,key,value, null);
if (binCount >=TREEIFY_THRESHOLD -1)// -1 for 1st
treeifyBin(tab,hash);
break;
}
if(e.hash== hash &&
((k = e.key) == key || (key !=null && key.equals(k))))
break;
p = e;
}
}
if(e != null) {// existing mapping for key
VoldValue = e.value;
if (!onlyIfAbsent || oldValue ==null)
e.value= value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//修改数据结构次数,用于快速迭代失败
if (++size> threshold)
resize();//插入之后,判断元素大于一定值时,就需要重新分配tab的大小,调整数据的位置结构。为什么不先调整再插入元素呢?因为不知道是插入数据还是更新数据。这个threshold是怎么来的呢。
afterNodeInsertion(evict);
return null;
}
三、调整数据位置,threshold的前世今生
若使用new HashMap()无参构造函数,则threshold为0,否则使用new HashMap(int capacity, float loadFactor),则threshold被设置为新值
this.threshold= tableSizeFor(initialCapacity);查看此方法
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(intcap) {
int n = cap -1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n <0) ?1 : (n >=MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : n +1;
}
这方法比较有意思,也是使用二进制的思路来处理。前面我们在定坑位的时候也使用位操作(这个比较简单理解)。而这个稍复杂一点。其实将最高位1一直向后复制,最后再加1。最后的结果就是和n最接近的2的N次幂。
再回到上面if(size>threshold),则重新改变结构。即改变结构的限制不是你在构造函数时传入的容量值capacity。
若使用HashMap()无参构造方法,那这里是不是有问题呀?当然不会了。因为在put的时候判断若tab为null的或空的,将会resize()。这时resize()方法里肯定有改变。
在看resize()方法前,我们知道size>threshold时进行重构数据,那这个threshold是什么东东?是我们指定的initialCapacity吗?
table的初始容量是大于等于初始容量值,却小于等于最小的2的N次幂,且是成倍增长的。threshold是table的size*loadFactor取整。这里不要被构造函数里的threshold=tableSizeFor(initialCapacity)这个所迷惑。
final Node<K,V>[]resize() {
Node<K,V>[] oldTab =table;
int oldCap = (oldTab ==null) ?0 : oldTab.length;
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 >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;// double threshold
}
else if(oldThr > 0)// initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap =DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR* DEFAULT_INITIAL_CAPACITY);
}
if(newThr == 0) {
floatft = (float)newCap *loadFactor;
newThr = (newCap <MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY?
(int)ft : Integer.MAX_VALUE);
}
threshold= newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])newNode[newCap];
table= newTab;
if (oldTab !=null) {
for(intj = 0;j < oldCap;++j) {
那么HashMap map=new HashMap(1);在初始化时,table=null,threshold=1,loadFactor=0.75f。在调用put时,就会执行resize(),因为table为null。此时分配table的容量为1,threshold=1*0.75f=0,然后直接将数据放进去,然后判断size为1,大于threshold(为0),此时再resize(),容量翻倍变成2,threshold变成1(2*0.75),即数组由一个长度,然后换成另一个长度为2的新的数组,并且重新分配数据。即new HashMap(1)这样最好不要用,还不如直接new HashMap(2)。若目的只是放一个元素,最高效的写法,还不如写电脑new HashMap(1, 1);即不要使用默认的加载因子。
下面表格表示new HashMap(1);每次放入新成员的操作过程
HashTable需要增加一种功能。类似于StringBuild的方法ensureCapacity(intminimumCapacity),在有些场景下减少resize()的次数。