ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

HashMap稍微详细的理解

2020-12-07 16:01:35  阅读:177  来源: 互联网

标签:key hash HashMap 稍微 next 链表 详细 null 节点


此文章用来记录hashmap的一些特点(在学习中的所了解的,如有不足,请指正)

什么是hash表

概念

先来一段百度百科的的解释

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

所谓的hash表在我看来嘛就是映射嘛,以前嘛要查找一个数或者一个值嘛是通过遍历的形式,这样的话就会有一个问题,那就是太浪费时间了,时间效率非常低,也不能非常低嘛,时间复杂度是O(n)。于是呢,人们为了更快的找到需要查找的值呢就想到了一种办法,将存储的位置与存储的值对应起来,这样查找的效率不就高了很多。但是怎么转换呢,聪明的人类想到了一种办法,利用一种函数映射的形式来解决,这个映射用的函数就叫做hash函数,这个表呢就叫hash散列表,但是呢这是有问题的。那就是

hash表冲突

很好理解嘛,不同的值可能经过hash函数生成同样的索引,这样的话就有冲突了,怎么解决?请看

hash表冲突的解决

我所了解的常用的

  • 直接寻址,也叫开放地址法,就是这个不能放我不放了,我放到下一个去,要是下一个还有就继续往后直到找到可以插入的位置,要是都没有,那就考虑一下扩容呗
  • hash再散列,就是用别的hash算法再算一遍
  • 拉链法,这个方法就是hashmap中用到的方法。不是有冲突嘛,统统拿来,统统放这,一个别想跑。其实就是利用链表,冲突了就追加节点(不是同一个的话才追加)
  • 建立公共溢出区,就是冲突了嘛,没坑了,那就走吧,不要呆在这里了

以上就是我所了解的,估计也是常用的吧,不然我也不会了解

HashMap

map的意思嘛,就是映射,才不是地图。Java中的HashMap就是利用hash表加链表实现的K,V形式的数据结构,和python中的字典是一样的。hashmap中的hash冲突的解决利用的是拉链法。1.7之前的拉链是只有链表,而在1.8增加了一个红黑树结构,这是因为,当链表长度太长的时候查找效率比较低。所以在hash桶数据的容量大于等于64以及hash桶内的元素数量大于等于8时就会转换为红黑树。今天我们进入源码一探究竟,先来看个静态常量

static final int MIN_TREEIFY_CAPACITY = 64;

树化:treeifyBin

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
//    如果说table 为null或者说容量小于64就扩容,不执行树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
//        如果说 所在的位置表不为空
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> 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);
    }
}

上面的就是进行树化的操作了,那么怎么才能树化呢

扩容:resize()方法

先准备一个重要的方法,resize()方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//判断老表是否为null为null的话长度就是0
    int oldThr = threshold; // 保存原来的老阈值
    int newCap, newThr = 0; //先将新表的长度 阈值设置为0
    if (oldCap > 0) {
    //如果说老表的容量大于0且容量大于等于最大容量(MAXIMUM_CAPACITY = 1 << 30)
    //就将阈值设为Integer.MAX_VALUE,然后直接返回也就是不再扩容了,仅仅将阈值增大就行了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
         //如果说老容量乘以2小鱼最大容量以及大于等于默认的容量( DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16)
        // 就将原来的阈值也扩充为两倍 就是说这里没啥意外容量就定下来了,也是一般的扩容情况
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //如果说老表的容量小于等于0,但是老阈值大于0,就将新的容量设置为老阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //如果老表的容量以及老阈值都不大于0,就执行初始操作,将新表的容量设置为16,计算新表的阈值
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果说得出的新表阈值等于0的话就用新表的容量乘以负载因子,然后如果说新表的容量小于最大值以及新的阈值小于最大值,就将新阈值设为所求,否则就是Integer.MAX_VALUE
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
           MAX_VALUE       (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) {
        //这里对老表进行遍历,采用for循环
        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;
                //如果说e是treeNode节点,也就是说,这个hash桶里边的节点已经树化过了
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //下面这个else的意思是如果有下一个节点而且没有树化,也是说是链表形式的至少有两个节点
                else { // preserve order
                    //这里就是将链表分成两种一种是高位链表一种是低位链表,至于什么是高低位链表,咱们往下看
                    //loHead低位头节点
                    //loTail低位尾节点
                    Node<K,V> loHead = null, loTail = null;
                    //hiHead高位头节点
                    //hiTail低位尾节点
                    Node<K,V> hiHead = null, hiTail = null;
                    //next节点
                    Node<K,V> next;
                    //开始do..While循环,因为肯定有next节点嘛,不然也到不了这里
                    do {
                        //保存一下next节点
                        next = e.next;
                        //注意这个与的 是与原来的容量进行比较的没有进行减一哈,减一是求索引用的。
                        //这里的意思举个例子来说就是比如原来的容量就是16吧,因为这里是位运算嘛,转换成二进制就是10000
                        //因为这里是等于0 的情况嘛,所以就假设e.hash二进制为1011001111吧,索引算出来就是1111
                        //运算开始 因不足用0补齐嘛
                        //1011001111
                        //     10000
                        //----------
                        //0000000000
                        //嗯 就是这种情况,因为原来容量二进制是5位也就是说如果hash值第五位是0,那么就扩容以后不会有任何变化
                        //因为扩容是变为原来的2倍,也就是左移一位变为100000。
                        //那么减1以后就是11111,刨去后边的4个1,两个最高位都是1也就是相同的,可以直接运算
                        //如果说此时元素的hash值在这个最高位是0的话,那么算出的索引与原来是一样的,这也就是低位索引
                        //这里只是将低位放在一起
                        if ((e.hash & oldCap) == 0) {
                            //如果尾节点为空就初始化(说明头节点也没值)
                            if (loTail == null)
                                //这里的头节点指示头所在的位置,以后追加就是用为节点了,高位链表一样如此
                                loHead = e;
                            else
                                //让尾节点的next指向e,
                                loTail.next = e;
                            //然后尾节点向后移一位
                            //这里写成loTail=loTail.next我感觉比较好理解一些
                            loTail = e;
                        }
                        //如果说不是0的话,说明hash值的高位是1,经过运算后就是11111就是原来的索引加上2^4
                        //就是原来的表的长度,所以高位链表只需要原来的索引加上原来的表的长度就是新的索引
                        //这里只是将高位放到一起
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //循环结束后,如果说低位链表不为空的话就说明执行了分高低位的工作,而且有低位的存在
                    //然后只需要将hash桶的节点指向低位链表的头节点,而且因为是低位链表嘛,索引跟原来的一样
                    if (loTail != null) {
                        //这里将为节点的next设置为null,因为这再遍历的时候尾节点的next与尾节点指向同一个位置
                        //因为已经遍历完了嘛,next也就没有值了,所以就清空。高位链表类似
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //这里判断高位链表是否为空,空的就说明没有高位链表嘛
                    //不空的话就将原来的索引加上老表的容量,至于为什么,上面已经解释过
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //至此返回新的链表
    return newTab;
}

你以为到这里就结束了?no,no,no,还有一个重要的方法,那就是如果是红黑树的话,怎么进行操作呢,就是这个红黑树节点里边的split(this, newTab, j, oldCap)方法了

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, 
                 int bit//这个bit什么意思呢,我猜是老表的容量,上面传过来的oldCap
                ) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    //开头雷击,梦回链表
    //这怎么和链表的操作差不多呢,也是分成高低位
    //其实注释上面也写了嘛
    //将树箱中的节点拆分为较高和较低的树箱,如果现在太小,则取消树化。 
    //为什么红黑树的节点也可以这样呢,因为
    /**
     static final 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
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
    */
    //所以有个成员变量是next,那不就跟链表一样了
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    //这个lc吧 low count:低位的数量
    //hc呢 high count:高位的数量
    int lc = 0, hc = 0;
    //显而易见,这里遍历treeNode
    //这里为什么不用while循环呢,是不用在外面声明变量嘛
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        //这里高低位一分,对应的count++就不展开细说了
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }

    if (loHead != null) {
        if (lc <= UNTREEIFY_THRESHOLD)
            //如果说数量小于等于UNTREEIFY_THRESHOLD=6,就弄成链表
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            //如果不小于6且高位链表不为null就树化
            //为啥需要高位链表不为空呢,
            /**
            这里个人理解,高位链表如果为空,说明旧数组下的红黑树中的元素在新数组中仍然全部在同一个位置,
            且先后顺序没有改变,也就是注释中的已经树化了,没有必要再次树化;而当高位节点不为空,
            说明原链表元素被拆分了,且位红黑树节点个数大于6,不满足转链表条件,需要重新树化。
            此处来自https://blog.csdn.net/hengwu1817/article/details/107095871/ 
            */
            //下面的高位链表也是如此
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

插入数据 put

调用put(k,v)方法实际上调用的是putVal方法

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

所以只需要分析putVal方法即可

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,//是否不存在才插入
               boolean evict//文档给的是创建表的模式,我的理解是可读可写
              ) {
    Node<K,V>[] tab; 
    Node<K,V> p; //p是table[i]所在的头
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//如果说原来的表不存在或者为空就执行resize()方法,上面已经进入看了一下
    //如果说原来的表的位置等于空的话就直接放进去 不存在冲突
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //这里才是重点 到这里说明发生冲突
        Node<K,V> e;//e表示要要插入的节点
        K k;
        //这个判断是看原来的老的hash值跟我传进来的hash值是否相同并且key也相同 或者说key不为空并且相同
        //也就是判断一下是不是相同的key 是的话就将p赋值给e
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //不是就判断一下头节点是否是红黑树节点
        //是的话就进行红黑树的操作
        else if (p instanceof TreeNode)
            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);
                    //如果说大于等于8-1也就是7的话就树化 因为要插入元素了嘛,所以插入以后就等于8了
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //这里key相同的话就中断
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //找到了要插入的节点后 如果e不为null 说明key是一样的 只需要替换一下值就好了
        if (e != null) { // existing mapping for key
            //保存一下旧的值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                //如果不是存在就不写的情况就换成新的值
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //记录一下修改次数,这个有个作用就是快速失败
    ++modCount;
    //然后判断是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

对于1.7的hashmap的死循环问题以及本篇文章的1.8的死循环数据覆盖问题,以后再总结

标签:key,hash,HashMap,稍微,next,链表,详细,null,节点
来源: https://www.cnblogs.com/clion/p/14097542.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有