JDK1.8 HashMap 学习笔记

前言


  本文主要根据jdk1.8的HashMap实现做的一次学习笔记,jdk1.7的HashMap与jdk1.8的实现有一些不同,但大体原理也是一致的。

HashMap实现原理


   Map主要应用的场景是Key-Value键值对的存储,如果当Key-Value键值对的数量达到一定数量的时候,必然会影响我们的查询效率,而大家使用HashMap的时候并没有显著的感觉查询效率显著的下降,主要是HashMap的实现方式。通常我们再进行数据查找的时候都是进行Key比对的方式查找,** 这样带来的问题就是如果有100w个Key那么就要查找100w次(最坏情况)。而hashMap的查找方式是将100w个数据分成了N个数据桶,将带有某些特征的数据放到一个桶里(Hash散列), 那么100w个数据分散到每一个数据桶里就成了100w/N个数据(分布均匀的情况下),查找次数就缩减到了100w/N次(最坏情况) **。

如下所示

  • 数组方式
      arr[]:
1
2
3
4
5
6
7
8
9
  • Hash方式
    map:
    {
       arr[0]:
1
2
3

          arr[1]:

4
5
6

          arr[1]:

7
8
9

        }
像arr[0],arr[1],arr[2]我们将他们称为哈希槽,每个哈希槽里面是一个链表结构
在这里也要说明下:HashMap的Hash槽并不是初始化之后数量就不会变化了,HashMap的Hash槽会根据Map中元素数量的变化而进行调整(目前只会变大),这里有一个结论就是相同数据且均匀分布的情况下Hash槽的数量越多查询效率越高

HashMap内部结构


   刚才已经粗略的简绍了HashMap的存储结构, 其实jdk1.8的HashMap在arr[x]的位置上使用两种数据结构:** 单向链表和红黑树 **,这里在所属哈希槽数据量不大的时候会使用单向链表,默认每个哈希槽数量大于8的时候会转成红黑数的形式存储,在小于6的时候会有红黑树转换成单向链表(如果是红黑树的情况下)。

整体结构

粗略(因为位置问题,有一定的错误(hash槽最少也是16个,红黑树至少8个节点以上)),但是大体如下
HashMap简化结构图

每个Hash槽中元素结构

这里每个元素在jdk1.8中抽象为Node, jdk 1.7中是HashMap.Entry.
下图是jdk1.8 Node的实现(简写省略了很多方法):

class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        ...
}

红黑树的Node是Node的子类 TreeNode

class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        ...
}

HashMap属性说明


    
    /**
    * Hash槽
    */
    transient Node<K,V>[] table;

  	/**
     * 默认Hash槽的大小。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

	/**
     * table的扩容阈值,当table的Node数量大于此值就会进行扩容
     */
 	int threshold;	

    /**
     * Hash可扩容的最大值。
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 负载因子,这里我们上文说过Hash越多查询效率就越高但是也会带来一个问题,空间上会有一些浪费,负载因子的作用就是在空间和时间上做出一个更好的权衡,这里通常不需要更改,经过测试0.75是通常情况下比较理想的取值了。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    final float loadFactor;
    
    /**
  	 * 当Hash槽中的元素数量,大于了此值将会把链表结构转换成红黑树结构。
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 当Hash槽中的元素是以红黑树结构,当树的节点小于此值将会将红黑树转变成链表。
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 这里是转换成红黑树对于Map元素的总量限制,如果Map元素的总量小于此值,当进行链表转红黑树的时候只会进行一次resize()扩容。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

	/**
	* 每次修改Map时会进行modCount++操作,用于快速检查是否在迭代时有其他线程是否修改过Map元素
	*/
	transient int modCount;

这里大家有没有注意到一个小细节,就是他的很多默认值都是2的次幂,这里我先把原因告诉大家:主要是因为HashMap的hash方法需要保证尽量减少Hash数量对于hash值得影响,以及HashMap扩容时移动Node的便利。

HashMap方法说明


  • 构造方法
    1. public HashMap(int initialCapacity, float loadFactor)
      可以预先设置Hash槽数量和负载因子,这里如果预测Map要存大量数据时可以将initialCapacity设置的高些,不然移动元素会比较耗时。
    2. public HashMap(int initialCapacity)
    3. public HashMap()
    4. public HashMap(Map<? extends K, ? extends V> m)
  • hash方法
    hash方法主要是获得散列值,以确保key的均匀分布。
      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
    
    HashMap是根据hash方法返回的hash值来和(hash槽的数量-1)进行与(&)操作,HashMap为了消除hash槽数量对hash值的影响使每一次扩容的hash槽数量都是2的次幂(最低是16) 。这里为什么2的次幂可以减少影响呢?是因为2的次幂-1,16在二进制表示的话是(0000 1111)与运算是都为1的情况下结果才为1,那面最后影响结果的是hash方法获得的hash值。
    这里大家可能会有一些疑惑为什么要把得出的hash值用后16位异或前16位,这里是因为我们Hash槽通常数量都达不到高16位的数量,很多时候hash值得前16位并没有参加到hash槽定位的运算中,所以这里进行了一次高位与低位的运算来提高hash函数的分散性,以提高hash查找的效率,这里也叫扰动函数
    hash函数这里使用的是Object的hashCode方法,应该是属于加法hash。
    几种常见的hash函数:加法hash,乘法hash,除法hash。
  • tableSizeFor方法
    tableSizeFor方法主要用于保证Hash槽的数量是2的次幂。
static final int tableSizeFor(int cap) {
        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;
    }

这个方法主要是确保用户初始化HashMap时传进的initialCapacity是2的次幂,即使初始化的时候调用了initialCapacity(30),HashMap也会根据此方法来获取initialCapacity向上最接近的2的次幂,至于为什么要保持2的次幂请参考hash方法,以及put方法和reszie方法

  • put方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

	/**
	* onlyIfAbsent : 表示是否可以替换已存在的元素(true不可以,false可以)
	* evict: 表示是否是创建模式
	*/
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
	                   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; //判断hash槽是否初始化,如果没初始化则先调用resize方法
	        if ((p = tab[i = (n - 1) & hash]) == null)  //确定Hash槽位置,使用hash槽数量-1 与 hash值
	            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 (p instanceof TreeNode) //如果此Hash槽的元素是红黑树结构则使用红黑树的put方法(具体暂时忽略)
	                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
	            else {
	                for (int binCount = 0; ; ++binCount) {
	                    if ((e = p.next) == null) {
	                        p.next = newNode(hash, key, value, null);
	                        if (binCount >= TREEIFY_THRESHOLD - 1) //如果大于指定的红黑树阈值,则将此Hash槽的Node转换成红黑树的形式
	                            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
	                V oldValue = e.value;
	                if (!onlyIfAbsent || oldValue == null)
	                    e.value = value;
	                afterNodeAccess(e); //回调,可以重写此方法监听
	                return oldValue;
	            }
	        }
	        ++modCount; //增加乐观锁,这个主要用于迭代时,其他线程修改了Map时的快速失败
	        if (++size > threshold) //判断是否需要扩容
	            resize();
	        afterNodeInsertion(evict); //回调,可以重写此方法监听
	        return null;
	    }

这里put流程可以参考后面的存储流程图
这里主要的就是key的Hash槽定位(n - 1) & hash,要理解为什么要与hash值?为了定位Hash槽,(n-1)与上任何数都是小于n-1的,并且n是2的次幂的话,结果的值就只跟hash的值有关。

  • resize方法
    resize方法主要负责hash槽的初始化,扩容,以及node元素在扩容后的迁移
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//查看Hash槽是否初始化
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) { //已初始化的情况
            if (oldCap >= MAXIMUM_CAPACITY) { //判断hash槽扩容后是否超过了最大值,若超过就不在进行扩容,并且调整阈值。
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 获得新的hash槽的数量值和新的hash槽阈值
        }
        else if (oldThr > 0) // 初始化用于HashMap(Map map)构造方法的情况
            newCap = oldThr;
        else {               // 初始化赋予HashMap默认值
            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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {//扩容前Hash槽内数据不为空的情况下需要移动Node节点。
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//红黑树的打散,这个方法就先不做分析。
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
                    //链表的Node数据的Hash转移。
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //判断扩容后是否需要移动,扩容前是hash值 * (n-1),由于n是2的次幂,那么扩容后Node所属hash槽的位置只能是不变或者加之前的n-1
                            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;
    }

这里可以参考后面的扩容流程图
resize方法比较有困惑的地方就是(e.hash & oldCap) == 0这行代码,这里主要是因为n是2的次幂
举个例子:
假如之前是16个Hash槽 二进制表示是:0001 0000。 n-1则为: 0000 1111
假设这个key的hash值为 0010 1110 那么和n-1 & 的值为 0000 1110。
下面我们把Hash槽扩容为32 二进制表示是: 0010 0000。 n-1则为:0001 1111
那么key的hash值 & n-1 的值为 0000 1110。 这里只要计算二进制的第5位就可以了,因为后面的数字n-1不变,hash值的二进制也不会变。之前是由于n-1只有4位有效数字,现在扩容后n-1有5位有效数字,所以我们只要把hash值的第五位加进来参与运算即可。
这里获得的Hash槽位置只会是不变或者是之前的位置加上(old n-1)。

HashMap存储流程


Created with Raphaël 2.2.0 传入参数Key,Value 计算Key的Hash值 哈希槽是否已经初始化? 根据Hash值确定具体的Hash槽位置 该哈希槽是否有元素? 当前Node节点的Key 是否与参数Key相等? 将新的value值复制给当前Node节点 当前容量是否需要扩容? 对Hash槽进行扩容 完成 当前Node节点是否 是红黑树节点? 使用红黑树的插入节点方式 插入新的Key,Value 轮询当前Node节点的下一个节点, 如果Key像等则替换Value值, 如果不相等且是最末尾节点的话添加新的Node节点。 当前链表长度是否要 转成红黑树结构? 将链表转换成红黑树 生成一个新的Node节点 添加到Hash槽中 yes no yes no yes no yes no yes no yes no

HashMap扩容流程


Created with Raphaël 2.2.0 获得当前table容量和table提升阈值 当前table容量大于0? 当前table容量是否是最大? 1 1 1 1 完成 扩大table容量值为之前 的2倍,提升阈值为之前的2倍 新的阈值是否为0? 新的table容量值乘以 负载因子获得新的阈值 当前table的node数量大于0? 获得老的Hash槽中的下一个元素 是否还拥有下一个Hash 扩容前的节点? 当前Hash槽是否为空? 当前节点是否是红黑树节点? 红黑树移动操作(暂时省略) 或得链表下一个Node 是否还拥有下一个 Node节点? 节点是否需要移动? 移动Node到(hash槽+老的Hash槽数量) 的hash节点上 当前阈值大于0 ? 将当前阈值赋值给新的table容量值, 这个情况的产生是通过调用HashMap(Map map)的方法产生的 初始化table容量值和阈值为默认值。 yes no yes no yes yes no yes no yes no yes no yes no yes no yes no

写在最后

这篇文章主要是针对HashMap源码解析后的笔记。
学习之后的收获是:

  1. hashMap对于确定Hash槽位置的整体布局与操作。
  2. hashMap整体结构的了解。
  3. hashMap的性能调整(可以基于初始Hash槽和加载因子)。

如果有什么错误,欢迎大家及时指正。

主要内容:本文详细介绍了一种QRBiLSTM(分位数回归双向长短期记忆网络)的时间序列区间预测方法。首先介绍了项目背景以及模型的优势,比如能够有效利用双向的信息,并对未来的趋势上限和下限做出估计。接着从数据生成出发讲述了具体的代码操作过程:数据预处理,搭建模型,进行训练,并最终可视化预测结果与计算分位数回归的边界线。提供的示例代码可以完全运行并且包含了数据生成环节,便于新手快速上手,深入学习。此外还指出了模型未来发展的方向,例如加入额外的输入特性和改善超参数配置等途径提高模型的表现。文中强调了时间序列的标准化和平稳检验,在样本划分阶段需要按时间序列顺序进行划分,并在训练阶段采取合适的手段预防过度拟合发生。 适合人群:对于希望学习和应用双向长短时记忆网络解决时序数据预测的初学者和具有一定基础的研究人员。尤其适用于有金融数据分析需求、需要做多一步或多步预测任务的从业者。 使用场景及目标:应用于金融市场波动预报、天气状况变化预测或是物流管理等多个领域内的决策支持。主要目的在于不仅能够提供精确的数值预计还能描绘出相应的区间概率图以增强结论置信程度。 补充说明:本教程通过一个由正弦信号加白噪构造而成的简单实例来指导大家理解和执行QRBiLSTM流程的所有关键步骤,这既方便于初学者跟踪学习,又有利于专业人士作为现有系统的补充参考工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值