Java容器之HashMap源码分析1

上一个笔记中分析了HashMap的大概结构以及基本用法。这一次笔记就再深入了解一下底层的实现细节。我们首先从hash函数以及扩容过程讲起,然后再了解一下链表数据结构以及红黑树的实现。 

hash函数 

hash音译为哈希,学名称为散列,功能是将任意长度的输入通过散列函数变换为固定长度的输出。HashMap在Java7中被设计为“线性表+链表”的数据结构,在Java8中被设计为“线性表+链表/红黑树”的数据结构。在HashMap中以散列码作为节点的位置标识,不同散列码被映射为线性表的索引。
散列码的空间是有限的,而输入空间可能是无限的,因此计算索引的过程实际上是压缩的过程,将大空间的输入映射到小空间,这样一来就不可避免的发生散列冲突,也就是可能出现两个不同的输入映射为相同的散列码,或者不同的散列码映射为相同的索引。
在HashMap中往往数组的大小有限,初始HashMap的table长度仅为16,因此很容易会发生散列冲突。这时就需要解决散列冲突,一般来说解决散列冲突的方法有以下几种:
  • 开放定址法:发生散列冲突之后,寻找下一个散列地址,也就是加一操作,只要散列空间足够大,就能寻找到空的位置。
  • 链地址法:将table的每个节点作为链表的头节点,发生散列碰撞之后,可以将新的元素插入到链表的尾部
  • 再哈希法:出现散列碰撞之后,用定义好的第二个哈希函数再次计算散列,直到不发生冲突。
  • 建立公共溢出区:将哈希表分为基本表和溢出表,发生散列冲突的元素全部存入溢出表中。
HashMap采用的是 链地址法,在Java7以前采用的是链表结构,在Java8中为了提高索引的效率,引入了红黑树的数据结构,当链表增长到一定长度时转换链表为红黑树。当然,一方面我们引入处理散列冲突的方法,提高散列冲突处理下数据结构的索引效率,另一方面我们还要想办法减少散列冲突的发生。在HashMap中,一般是将散列值除以数组长度,取余数为下标,这个方法也可以写成按位&操作 。
// 下面两种运算都是利用散列值对数组长度取余,按位与操作基于内存,效率更高
length % n
(length-1) & n

在Java8中,为了更方便应用按位操作,数组长度往往都是2的幂次,因此可能会出现如下的情况,只要低位保持一致,则无论高位如何变化,最终的索引都是一样的。这样就形成了周期规律,与散列的原则不符。 
0000 0000 0000 0000 0000 0000 0001 0011 n1=19
0000 0000 0000 0000 0011 1111 1111 1111 length-1
0000 0000 0000 0000 0000 0000 0001 0011 19 = 19 % length

0100 1000 0000 0000 0000 0000 0001 0011 n2
0000 0000 0000 0000 0000 0000 0001 1111 31
0000 0000 0000 0000 0000 0000 0001 0011 19 = n2 % length

于是HashMap中引入了hash方法,对hashCode进行扰动,打破周期规律,操作方法是将原hashCode右移16位,然后进行或运算,这个操作是将高16位与低16位进行或运算,然后将高16位置零,这样一来,高位数据的影响就可以引入到哈希码的计算中,也能打破周期规律。 
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
0000 0000 0000 0000 0000 0000 0001 0011  n1=19
0000 0000 0000 0000 0000 0000 0000 0000  n1>>>16
0000 0000 0000 0000 0000 0000 0001 0011  n1 ^ (n1>>>16)
0000 0000 0000 0000 0011 1111 1111 1111  length-1
0000 0000 0000 0000 0000 0000 0001 0011  19 = n1 % length

0100 1000 0000 0000 0000 0000 0001 0011  n2
0000 0000 0000 0000 0100 1000 0000 0000  n2>>>16
0100 1000 0000 0000 0100 1000 0001 0011  n2 ^ (n2>>>16)
0000 0000 0000 0000 0111 1111 1111 1111  length-1
0000 0000 0000 0000 0100 1000 0001 0011  不再是19

扩容过程 

当HashMap存储元素超过一定容量时,就会调用resize方法进行扩容。首先我们看下触发扩容的条件: 
// putMapEntries
/* s为待插入的Map集合的size,当待添加的元素个数超过阈值,则开始扩容 */
else if (s > threshold)
    resize();

// putVal
/* 当table为null或者table的长度为零,这时需要通过resize进行初始化 */
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
/* 添加元素之后,若size超过阈值,则开始扩容 */
if (++size > threshold)
    resize();

// treeifyBin
/* 在树化操作里,若table为null或者table的长度小于最小树化容量,则开始扩容 */
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();

// computeIfAbsent
/* 若size超过阈值或者table为null或者table长度为0,则进行扩容或者初始化 */
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
    
// compute
/* 若size超过阈值或者table为null或者table长度为0,则进行扩容或者初始化 */
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

// merge
/* 若size超过阈值或者table为null或者table长度为0,则进行扩容或者初始化 */
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

由上述调用resize的源码可以看出,启动扩容的情况有如下几种:
  • table未初始化,即table为null,或者长度为0
  • size大于阈值,size是指存储的元素个数,而非table的长度
  • 树化操作前,table的长度小于最小树化容量 

那么HashMap的扩容过程是如何进行的呢?且看resize方法的源码
final Node<K,V>[] resize() {
    // 定义局部变量存储旧table,旧容量,旧阈值
    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)
            // 新容量 = 2 * 旧容量, 且保证新容量小于最大值,并且旧容量大于16
            // 新阈值 = 2 * 旧阈值
            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"})
        // 初始化新table
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将新table赋值给table
    table = newTab;
    
    /* 进行数据迁移 */
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) { // 遍历旧table
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; // 旧表置为null
                if (e.next == null)
                    // 若table中该位置只有一个节点,无链表或者树
                    // 则将该节点按新索引迁移至新表
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 若table中该位置节点为树节点
                    // 则对树进行操作,调用split方法将树的数据进行迁移
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 若table中该位置节点为链表头节点
                    // 则遍历链表,将链表进行拆分并迁移到新的table中
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.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;
                    }
                }
            }
        }
    }
    return newTab;
}

这里关于链表拆分的代码很有意思,单独拎出来看一下 
// 定义两个新的链表,名为lo链表、hi链表。用于存放拆分后的链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;

// 循环遍历链表,并进行拆分
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        // 如果e.hash & oldCap == 0, 将节点添加进lo链表
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        // 将节点添加进hi链表
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);

// 如果lo链表非空
// 将lo链表添加到new table中
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
// 如果hi链表非空
// 将hi链表添加到new table中
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

上述代码分析很简单,具体的步骤就是将一个链表拆分成两个链表,然后分别放置在新table中。拆分出来的两个链表中,lo链表头部节点在table中的位置不变,hi链表头部节点在table中的位置后移oldCap位。这一点设计的十分巧妙。
另外,需要注意的是,HashMap中所指的容量均为线性表的长度,而size指的才是元素的个数。 
newCap = oldCap << 1
oldCap 010000   2^4
newCap 100000   2^5

索引运算公式  index = hash & (Cap-1)
oldIndex hash & 001111
newIndex hash & 011111 两者的区别在于第4位
也就是说如果hash值第4位为0,则newIndex = oldIndex
如果第4位为1, 则newIndex = oldIndex + 2^4 = oldIndex + oldCap

所以我们通过  hash & oldCap(010000) 来判断第4位是否为零
划分示意图如下: 

猜你喜欢

转载自www.cnblogs.com/zhengshuangxi/p/11061842.html