美文网首页
HashMap解析(2)

HashMap解析(2)

作者: 迷糊小生 | 来源:发表于2019-04-05 14:05 被阅读0次

上一篇对HashMap的结构做了详细的介绍,讲解了put方法还有get方法,本篇将会更深入的走进HashMap源码。

散列函数(解释hash碰撞)

   public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

在上篇文章我们讲解的get和put方法中都使用了hash(key),现在让我们一起来看下这个hash函数究竟是什么吧。

   /**
     * 将高位与低位进行与运算来计算哈希值。因为在hashmap中使用2的整数幂来作为掩码,所以只在当前掩码之上的位上发生
     * 变化的散列总是会发生冲突。(在已知的例子中,Float键的集合在小表中保持连续的整数)因此,我们应用一个位运算
     * 来向下转移高位的影响。 这是在综合考虑了运算速度,效用和质量之后的权衡。因为许多常见的散列集合已经合理分布
     * (所以不能从扩散中受益),并且因为我们使用树来处理bin中发生的大量碰撞的情况,所以我们尽可能以代价最低的方式
     * 对一些位移进行异或运算以减少系统损失, 以及合并由于hashmap容量边界而不会被用于散列运算的最高位的影响。
     */
      //将key的hashCode的高16位与低16位做了一个异或操作
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
image.png

n是table的大小,默认是16,二进制即为10000,n - 1 对应的二进制则为1111,这样再与hash值做“与”操作时,就变成了掩码,除了最后四位全部被置为0,而最后四位的范围肯定会落在(0~n-1)之间,正好是数组的大小范围,散列函数的妙处就在于此了。

image.png

就拿昨日我们的例子做实验进行计算:

“噢噢”的hash值为707658,转化为二进制就是:10101100110001001010,table大小是32,n-1也就是31对应的二进制则是:11111


image.png

运算之后的结果是:01010,也就是10,所以“噢噢”的下标就落在了table的10号位

再来看“啊啊”,它的hash值为698698,对应的二进制是10101010100101001010,table大小是32,n-1也就是31对应的二进制则是:11111


image.png

运算之后的结果是:01010,也同样是10号位,因此发生了hash碰撞,形成了链表。

扩容函数

在之前我们看过了HashMap中put方法的源码,我们可以经常看到resize()这个方法,这个方法是用来对hashmap进行扩容的,现在让我们一块来对其分析一下

/**
   * 初始化或将table的大小进行扩容。 如果table为null,则按照字段threshold中的初始容量目标进行分配。
   * 否则,因为我们使用2次幂进行扩容,所以在新表中,来自每个bin中的元素必须保持在相同的索引处,或者以原偏移量的2次幂进行移动。
   */
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //扩容前table容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //扩容前阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果旧容器的容量不为0
        if (oldCap > 0) {
            //如果扩容前的容量大于等于HashMap的最大容量,则将其阈值设置为2的31次方减1(MAXIMUM_CAPACITY的值为2的30次方,永远达不到阈值,不再对其进行扩容),并返回原容器
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新容器调整为原来的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //新容器的阈值也调为之前的两倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) //旧容器容器容量为0,但是阈值不为0时,初始容量被设置为阈值
            newCap = oldThr;
        else {               // 旧容器容器容量为0,阈值为0时,使用默认出初始化容器大小与阈值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
        //旧容器容器容量为0,但是阈值不为0时,此时新容器大小被赋予上了阈值的大小
            //给新容器设置新阈值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //将旧数组中的node重新散列到新数组中
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //节点不为null
                    oldTab[j] = null;
                    if (e.next == null)
                        //如果该节点为没有子节点,则直接放入到新table中
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //如果该节点为树节点,则对树的hash重新分配
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //如果该节点有子节点,为链表结构时
                        Node<K,V> loHead = null//低位头节点, loTail = null;//低位尾节点
                        Node<K,V> hiHead = null//高位头节点, hiTail = null;//高位尾节点
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //如果该节点为新容器的0号位
                            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;
    }

总结:HashMap使用的是懒加载的方式,在构造函数中并没有初始化table,而是再添加第一个数据的时候在扩容方法中初始化了table。

当使用put插入元素的时候,如果发现目前的bins占用程度已经超过了加载因子所设置的比例,那么就会发生resize,简单来说就是把原来的容量和阈值都调整为原来的2倍,之后重新计算数组索引,把节点再放到新的bin中。因为index值的计算与table数组的大小有关,所以扩容后,元素的位置有可能会调整。

拿之前的“订单”为例,在没有扩容之前:


image.png
image.png

经过计算在容量为8的时候的确在2号位上

但是,在扩容到size为64的时候我们再计算一下它的位置


image.png

经过计算它应该在34号位置上,而事实也正是如此,扩容后元素的位置发生了改变。

image.png

相关文章

  • HashMap解析(2)

    上一篇对HashMap的结构做了详细的介绍,讲解了put方法还有get方法,本篇将会更深入的走进HashMap源码...

  • 【16】 hashmap

    hashmap 1.7 和1.8的区别 hashmap全家桶 hashmap 源码解析 hashmap hashm...

  • LinkedHashMap源码解析

    一 成员变量解析 二 关键方法解析 1 添加元素 2 获取元素 LinkedHashMap 继承了 HashMap...

  • Java HashMap和线程安全Map

    参考: 面试必问-几种线程安全的Map解析 未整理 1. HashMap HashMap详细介绍(源码解析)和使用...

  • HashMap源码解析

    HashMap源码解析 前言 之前写过一篇SparseArray的源码解析,今天我们就对HashMap下手,撸一撸...

  • HashMap内部原理解析

    博文出处:HashMap内部原理解析,欢迎大家关注我的博客,谢谢! 注:本文解析的 HashMap 源代码基于 J...

  • 面试准备

    1.HashMap && CurrentHashMap源码分析 HashMap源码解析 java 并发编程之 Co...

  • ConcurrentHashMap 原理解析(JDK1.8)

    了解ConcurrentHashMap 实现原理,建议首先了解下HashMap实现原理。HashMap 源码解析(...

  • HashMap原理解析

    HashMap 原理解析 hashmap构造hashmap 默认的初始数组容量大小为16,默认加载因子为0.75,...

  • Java基础之LinkedList源码解析

    Java集合源码解析系列 Java基础之HashMap源码解析 Java基础之LinkedHashMap源码解析 ...

网友评论

      本文标题:HashMap解析(2)

      本文链接:https://www.haomeiwen.com/subject/igpciqtx.html