栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

HashMap简解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

HashMap简解

常见问题
  • 为什么map的容量是 2 n 2^n 2n

    • 我们知道,散列是将键值映射为一个索引(散列方法),以此来加快检索速度。那么将键值映射就需要一个有效的数组下标,通常情况我们会使用取余数的方法。如果这时候数组的大小为n,那么索引取值范围是[0…n-1],取余数操作就是
      i n d e x = h a s h % n index = hash % n index=hash%n
      如果是 2 n 2^n 2n的大小,我们可以使用&操作
      i n d e x = h a s h & ( n − 1 ) index = hash & (n-1) index=hash&(n−1)
      假设数组大小是n=8,hash为9,取余数索引值必然是0~7。 2 n 2^n 2n-1的二进制必然全是1,与全1进行&操作,1保留下来,0被置0,这样子的操作等同与取余且速度更快。

       1001 = 9
       0111 = 8-1
       -----------
       0001 = 1
      
  • 为什么树化阈值是8,逆树化为6?

    • 在加载因子为0.75的情况下,使用0.5的参数使用泊松分布。一个链表长度为8的情况已经是非常低了。
    • 逆树化主要是避免8,9之间来回树化带来的损耗。
  • 初始化大小?

    • 16
  • 最小树化容量?

    • 如果map的capacity容量小于64,就会进行resize操作,而避免树化。
  • hash()方法使用hashCode的低16位与高16进行异或,来计算hash值

      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
    
    • 在capacity的容量偏低的情况下,掩码去除高位字节,只保留低位的字节计算,从而常常导致冲突。例如:在小表中,使用Float作为key,并保留着连续的整数。
    • 目的:减少碰撞
    • 大概意思:table使用2的幂次作为掩码,散列只在一些字节上计算,就会导致常常发生冲突。又因为很多的散列集都很合理的分布了,所以使用了这个最简易的方式去减少系统的损失,并结合高位(前16)的影响来计算索引。否则那些高位索引由于边界限制,永远都不会参与到索引的计算中。
  • 计算cap算法

    static final int tableSizeFor(int cap) {
        int n = cap - 1;	//保证cap刚好是2的幂次时,不用增长
        n |= n >>> 1;	//最高第二位必然为1(这里指,从第一个1开始称最高)
        n |= n >>> 2;	//最高的四位必然是1
        n |= n >>> 4;	//同理8位
        n |= n >>> 8;	//同理16位
        n |= n >>> 16;	//同理32位
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    
公共 1 构造方法 2 常用方法
  • putVal

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //发生冲突
            Node e; K k;
            //与第一个节点相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //该容器已经树化
            else if (p instanceof TreeNode)
                e = ((TreeNode)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);
                        //节点数为9的时候,树化
                        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;
                }
            }
            //该键已有对应的value值
            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;
    }
    
  • resize:初始化或者是将容量扩倍

    //map不为空情况
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //翻倍,后一个判断是因为初始容量小于default的resize操作
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //map为空,但存在初始化容量
    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 (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node 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)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node 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;
                    }
                    //索引值多1,就是加上oldCap
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    

resize导致的列表中元素重新分配。由于容量加倍,也就是说8加倍到16,进行&操作也就是多了一个1。

以hash=[1, 9, 25],oldCap = 8,newCap = 16举例。
那么情况就是两种:
hash值:00001(1),01001(9),11001(25)。
旧索引: 00001(1),00001(9),00001(25)
新索引: 00001(1),00001(9),01001(25)。
多一位操作,意味着要么是0,要么是1。0的保持在原位,1的索引就刚好偏移的oldCap的大小

  • treeifyBin:树化容器,同时将节点进行前后关联。
    final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;
        //首先判断tab是否为空,或者是否达到树化的最小容量。
        //如果map的容量没有达到64个容器,那么将会resize,避免树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode hd = null, tl = null;
            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);
        }
    }
    
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/271498.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号