zoukankan      html  css  js  c++  java
  • 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位是否为零
    划分示意图如下: 
  • 相关阅读:
    CF117C Cycle (竞赛图找环)
    P1144 最短路计数 (bfs/SPFA)
    RabbitMQ.Client API (.NET)中文文档
    四元组
    .Net Standard Http请求实例
    .Net Standard简介
    Lambda表达式(lambda expression)⭐⭐⭐⭐⭐
    CSS
    工具类css框架
    Sass
  • 原文地址:https://www.cnblogs.com/zhengshuangxi/p/11061842.html
Copyright © 2011-2022 走看看