HashMap

JDK1.8

一、关键字段

//创建 HashMap 时未指定初始容量情况下的默认容量   
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//HashMap 的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//HashMap 默认的装载因子,当 HashMap 中元素数量超过 容量*装载因子 时,进行 resize() 操作
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//用来确定何时将解决 hash 冲突的链表转变为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 用来确定何时将解决 hash 冲突的红黑树转变为链表
static final int UNTREEIFY_THRESHOLD = 6;

/* 当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )导致的 hash 冲突太多,则不进行链表转变为红黑树操作,转为利用 resize() 函数对 hashMap 扩容 */
static final int MIN_TREEIFY_CAPACITY = 64;

//保存Node<K,V>节点的数组
 transient Node<K,V>[] table;

//由 hashMap 中 Node<K,V> 节点构成的 set
transient Set<Map.Entry<K,V>> entrySet;

//记录 hashMap 当前存储的元素的数量
transient int size;

//记录 hashMap 发生结构性变化的次数(注意 value 的覆盖不属于结构性变化)
transient int modCount;

//threshold的值应等于 table.length * loadFactor, size 超过这个值时进行 resize()扩容
int threshold;

//记录 hashMap 装载因子
final float loadFactor;

二、算法特点

1.容量初始化

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

threshold 为阈值。1.7版本直接取 initialCapacity ,1.8做了运算,查看 tableSizeFor 方法

/**
 * 找到大于或等于 cap 的最小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;
}

假设个数带入计算一下可以发现,其实就是将 cap 二进制的各位都转化为1,在最后返回前加上1,保证是2的幂

int n = cap - 1的作用是为了避免 cap 本来就是2的幂的情况,导致在 cap 的基础上又乘了次2

int 是32位,n 右移十六位后作或运算,正好将最高位以后的所有位都转化为了1,再右移32位(结果为0)作或运算就没有意义了

2.键在桶中位置

// 计算键的哈希值在哈希表(桶数组)中的位置
key.hash & (table.length-1)

等价于对桶数组长度取余

HashMap 中桶数组的长度 length 总是2的幂,这样在计算位置中,table.length-1的结果,二进制形式上各位都为1,在与 hash的&运算时,增加了结果的可能性,减少了哈希碰撞几率,让元素能够更均匀的分布在桶数组上。

3.哈希转换

/**
 * 计算键的 hash 值
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看这个方法的逻辑好像是通过位运算重新计算 hash,那么这里为什么要这样做呢?为什么不直接用键的 hashCode 方法产生的 hash 呢?

当哈希表容量不大时,单纯利用 hashCode 和容量进行 & 运算,hashCode 只有低位参与了计算。这样导致了计算结果只与低位信息有关,高位数据没发挥作用。

hashCode 方法产生的 hash 是 int 类型,32 位宽,前16位为高位,后16位为低位。

像上图中,让高位数据与低位数据进行异或,变相的让高位数据参与到计算中,以此加大低位信息的随机性。

除此之外,重新计算 hash 的另一个好处是可以增加 hash 的复杂度。当我们覆写 hashCode 方法时,可能会写出分布性不佳的 hashCode 方法,进而导致 hash 的冲突率比较高。通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。这也就是为什么 HashMap 不直接使用键对象原始 hash 的原因了。

4.扩容后重新计算在桶中位置

    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;
    }

resize 扩容方法中的一段,当旧的表中有元素或链表,且链表还未转化为红黑树,对新表进行重新映射的实现

e.hash & oldCap - 1是对表容量取模,计算key在表中位置

注意e.hash & oldCap此运算,因为表容量一直为2的幂,所以 oldCap 的二进制形式除最高位以外,其余低位都为 0

与键的哈希值作 & 运算,可以判断键对应位上的值是1还是0

当为1时,e.hash & newCap - 1计算下的新位置,是原位置加上 oldCap 的值

当为0时,依旧为原位置,不发生变化

可以假设值带入验证。这样将原链表分为了两组,每组都用一个 Head 和 Tail 引用,形成两条链表,引用在新表上

三、构造

//构造方法1,指定初始容量及装载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    
    //tableSizeFor(initialCapacity) 方法返回的值是最接近 initialCapacity 的2的幂
    //注意此种方法创建的 hashMap 初始容量的值存在 threshold 中
    this.threshold = tableSizeFor(initialCapacity);
}

//构造方法2,仅指定初始容量,装载因子的值采用默认的 0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造方法3,所有参数均采用默认值
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

四、插入

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // 初始化表
    if ((tab = table) == null || (n = tab.length) == 0)
        ...
        
    // 表对应位置为null,直接插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        ...
    // 表位置上已存在元素
    else {
        // 已存在,且在第一个位置上的情况
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            ···
        // 已树化的情况
        else if (p instanceof TreeNode)
            ···
        // 仍是链表的情况
        else {
            for (int binCount = 0; ; ++binCount) {
                // 没有此元素,开始插入
                if ((e = p.next) == null) {
                    ...
                    // 链表数已大于树化阈值,插入后开始树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    ···
                }
                // 链表中存在的情况
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    ...
                ...
            }
        }
        // 如果存在就进行替换
        if (e != null) { // existing mapping for key
           ...
        }
    }
    // 大于扩容阈值,开始扩容
    if (++size > threshold)
        ...
    return null;
}
  • 必要的话初始化表
  • 对应表中位置为null直接插入,不是null分情况讨论
  • 根据树化、链化不同,实现不同;链化时超过了树化阈值进行树化
  • 如果链表中或者树中已存在key,进行替换
  • 最后再判断元素个数是否大于表阈值进行扩容
//读懂这个函数要注意理解 hash 冲突发生的几种情况
//1、两节点 key 值相同(hash值一定相同),导致冲突
//2、两节点 key 值不同,由于 hash 函数的局限性导致hash 值相同,冲突
//3、两节点 key 值不同,hash 值不同,但 hash 值对数组长度取模后相同,冲突
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    TreeNode<K,V> root = (parent != null) ? root() : this;
    //从根节点开始查找合适的插入位置(与二叉搜索树查找过程相同)
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1; // dir小于0,接下来查找当前节点左孩子
        else if (ph < h)
            dir = 1; // dir大于0,接下来查找当前节点右孩子
        else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
            //进入这个else if 代表 hash 值相同,key 相同
            return p;
        /*要进入下面这个else if,代表有以下几个含义:
        1、当前节点与待插入节点 key 不同, hash 值相同
       2、k是不可比较的,即k并未实现 comparable<K> 接口
       (若 k 实现了comparable<K> 接口,comparableClassFor(k)返回的是k的 class,而不是 null)
       
       或者 compareComparables(kc, k, pk) 返回值为 0
       (pk 为空 或者 按照 k.compareTo(pk) 返回值为0,返回值为0可能是由于
       k的compareTo 方法实现不当引起的,compareTo 判定相等,而上个 else if 中 equals 判定不等)*/
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            //在以当前节点为根的整个树上搜索是否存在待插入节点(只会搜索一次)
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    //若树中存在待插入节点,直接返回
                    return q;
            }
            // 既然k是不可比较的,那我自己指定一个比较方式
            dir = tieBreakOrder(k, pk);
        }//end else if

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            //找到了待插入的位置,xp 为待插入节点的父节点
            //注意TreeNode节点中既存在树状关系,也存在链式关系,并且是双端链表
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            //插入节点后进行二叉树的平衡操作
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }//end for
}//end putTreeVal     

static int tieBreakOrder(Object a, Object b) {
    int d;
    //System.identityHashCode()实际是利用对象 a,b 的内存地址进行比较
    if (a == null || b == null ||
        (d = a.getClass().getName().
         compareTo(b.getClass().getName())) == 0)
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
             -1 : 1);
    return d;
}
  • 比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较
  • 检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较
  • 如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder

五、扩容

final Node<K,V>[] resize() {
    
    // 如果 table 不为空,表明已经初始化过了
    if (oldCap > 0) {
        
        // 当 table 容量超过容量最大值,则不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        
        // 按旧容量和阈值的2倍计算新容量和阈值的大小
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            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;
        ...
    }
    ...
    
    // 将旧桶数组中的数据映射到新桶中
    if (oldTab != null) {
        //把 oldTab 中的节点 reHash 到 newTab 中去
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //若节点是单个节点,直接在 newTab 中进行重定位
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //若是链表,进行链表的 rehash 操作
                else { // preserve order
                    ...
                }
            }
        }
    }
    return newTab;
}        
  • 根据是否初始化桶数组,使用的有无参构造,计算新桶数组的容量 newCap 和新阈值 newThr
  • 根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的
  • 将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。
// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    /* 
     * 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。
     * 下面的循环是对红黑树节点进行分组,与上面类似
     */
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        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) {
        // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            /* 
             * hiHead == null 时,表明扩容后,
             * 所有节点仍在原位置,树结构不变,无需重新树化
             */
            if (hiHead != null) 
                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);
        }
    }
}

从源码上可以看得出,重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。

不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。

如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。

否则根据条件重新将 TreeNode 链表树化。

参考

HashMap 源码详细分析(JDK1.8):https://segmentfault.com/a/1190000012926722?utm_source=tag-newest

JDK1.8源码阅读系列之四:HashMap (原创):https://www.cnblogs.com/Michaelwjw/p/6411176.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容