一、摘要
以下分析内容均是基于JDK1.8产生的,同时也和JDK1.7版本的hashmap做了一些比较。在1.7版本中,HashMap的实现是基于数组+链表的形式,而在1.8版本中则引入了红黑树,但其实好多内容都是相同的。
从上面图中可以看出,HashMap等于数组+链表+红黑树三者结合。当进来的数据被Hash后会得到一个数组的下标,从而可以找到对应的位置,当该数组元素存在元素时,则会相应的以链表的形式给出,同时我们想取出value值时也要相应对key进行equals才能找到相应的位置,当链表长度大于8时,则会转换成红黑树来表示。
二、源码分析
1、HashMap主要的成员值:
//源码英文注释均舍去//初始化Node数组容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始化最大的数组容量
static final int MAXIMUM_CAPACITY = 1 << 30;//初始化负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;//由链表转红黑树的临界值
static final int TREEIFY_THRESHOLD = 8;//由红黑树转链表的临界值
static final int UNTREEIFY_THRESHOLD = 6;//桶可能被转化为树形结构的最小容量的临界值
static final int MIN_TREEIFY_CAPACITY = 64;//计数器
transient intmodCount;//Node数组扩容的临界值,第一次为12
int threshold;
2、HashMap主要的构造方法
HashMap中有四个构造方法,但这四个构造方法主要的目的还是在于初始化数组的容量以及负载因子(这个变量涉及到数组的扩容的问题),下面仅拿一个构造函数来讲:
1 public HashMap(int initialCapacity, floatloadFactor) {2 //判断初始化数组的容量大小
3 if (initialCapacity < 0)4 throw new IllegalArgumentException("Illegal initial capacity: " +
5 initialCapacity);6 if (initialCapacity >MAXIMUM_CAPACITY)7 initialCapacity =MAXIMUM_CAPACITY;8 //判断初始化的负载因子
9 if (loadFactor <= 0 ||Float.isNaN(loadFactor))10 throw new IllegalArgumentException("Illegal load factor: " +
11 loadFactor);12 //初始化负载因子
13 this.loadFactor =loadFactor;14 this.threshold =tableSizeFor(initialCapacity);15 }
3、HashMap中主要的方法分析
a、putVal()方法
当我们使用map.put时,该方法会去调用putVal()方法
final V putVal(int hash, K key, V value, booleanonlyIfAbsent,booleanevict) {
Node[] tab; Node p; intn, i;if ((tab = table) == null || (n = tab.length) == 0)//会对该桶进行第一次初始化,桶的数组大小为16
n = (tab =resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)//判断桶的下标是否含有第一个元素,没有的话就放进去
tab[i] = newNode(hash, key, value, null);else{//桶的下标已经存在第一个元素了
Nodee; K k;//判断桶下标中存在的第一个元素的hash值和key值是否相等
if (p.hash == hash &&((k= p.key) == key || (key != null &&key.equals(k))))//相等的话则用e来进行记录
e =p;else if (p instanceofTreeNode)//hash值相等,key不相等则判断标中存在的第一个元素是否为树的节点//是的话则将元素添加到树节点上
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//hash值相等,key不相等放到链表中
else{for (int binCount = 0; ; ++binCount) {//判断该链表尾部指针是不是空的
if ((e = p.next) == null) {//在链表的尾部创建链表节点
p.next = newNode(hash, key, value, null);//判断链表的长度是否达到转化红黑树的临界值,临界值为8
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;
}
}//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时返回新来的value这个值
if (e != null) { //existing mapping for key
V oldValue =e.value;if (!onlyIfAbsent || oldValue == null)
e.value=value;
afterNodeAccess(e);returnoldValue;
}
}++modCount;//直到桶的数组大小超过了负载的临界值时,则进行扩容
if (++size >threshold)
resize();
afterNodeInsertion(evict);return null;
}
b、resize()方法
在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
final Node[] resize() {
Node[] oldTab =table;int oldCap = (oldTab == null) ? 0: oldTab.length;int oldThr =threshold;int newCap, newThr = 0;//判断旧的table大小
if (oldCap > 0) {if (oldCap >=MAXIMUM_CAPACITY) {
threshold=Integer.MAX_VALUE;returnoldTab;
}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) {float ft = (float)newCap *loadFactor;
newThr= (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
}//上面这些均是代表对桶的大小进行一些判断并初始化
threshold =newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])newNode[newCap];
table=newTab;//扩容之后对旧的桶进行重新分配,打散到其他的位置,使其均匀的分散
if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {
Nodee;if ((e = oldTab[j]) != null) {
oldTab[j]= null;if (e.next == null)
newTab[e.hash& (newCap - 1)] =e;else if (e instanceofTreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);else { //preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Nodenext;do{
next=e.next;//将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割//为0的话则保留在原始的位置//不为0的话则将其移动到原始位置+增加的数组大小(比如第二次扩容时,这时值就为16)这个位置上面
if ((e.hash & oldCap) == 0) {if (loTail == null)
loHead=e;elseloTail.next=e;
loTail=e;
}else{if (hiTail == null)
hiHead=e;elsehiTail.next=e;
hiTail=e;
}
}while ((e = next) != null);if (loTail != null) {
loTail.next= null;
newTab[j]=loHead;
}if (hiTail != null) {
hiTail.next= null;
newTab[j+ oldCap] =hiHead;
}
}
}
}
}returnnewTab;
}
c、treeifyBin()方法
在putVal()方法中,我们能够看到,当链表的长度大于TREEIFY_THRESHOLD这个临界值时,这个时候就会调用treeifyBin()方法,将链表的结构转化为红黑树结构,这也是JDK1.8版本新优化的功能点
在此方法中主要做了:
1、判断桶是否初始化、或者判断桶中的元素个数是否达到MIN_TREEIFY_CAPACITY阈值,没有的话则去进行初始化或者扩容
2、若不符合上述条件,则会对其进行树形化,首先会先去遍历桶中链表的元素,并创建相同的树节点,接着会根据桶的第一个元素而去创建树的头结点,并以此建立联系
final void treeifyBin(Node[] tab, inthash) {int n, index; Nodee;if (tab == null || (n = tab.length)
resize();//开始树形化
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;//对桶Node中的链表元素进行循环,从链表的头节点开始将链表的头元素改为树的头节点
do{
TreeNode p = replacementTreeNode(e, null);if (tl == null)
hd=p;else{//树的头节点不为空时
p.prev =tl;
tl.next=p;
}
tl=p;
}while ((e = e.next) != null);//将桶中的元素与树的头节点进行连接
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
三、细节注意
1、在上述方法中,我们经常看到在进行当前元素是否相同时会去进行判断,如果仅仅是对值的hashCode进行判断,当hash值相同时,则会发生Hash碰撞,这个时候利用链表的形式去解决hash碰撞的问题,当碰撞发生了,则会将元素存放在链表的下一个节点中,同时在判断两个是否是同一个元素时,需要去判断当且仅当hashCode()和equal都相等时才能判断这两个元素是相等的,两元素相同时则会用新的value替换掉旧的value值
2、在对桶进行扩容时,当桶的实际使用大小超多了0.75*桶的容量时,这个时候要对其进行扩容,同时扩容之后原桶上的元素的位置也会从新被打散,其判断条件是通过值的hash与上原始的容量,若等于0则停留在原始的位置不动,若等于1则新的位置=原始的位置+新增了多少个数组
3、当链表的长度大于8时,这个时候就需要将链表树形化转换成红黑树
4、根据(n - 1) & hash来判断桶的数组大小最好是2的幂次方,如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了