基于JDK1.8的HashMap源码分析

特点

  • 根据键的 hashCode 值存储数据

  • 遍历顺序不确定

  • 允许 key 和 value 为 null

  • 非线程安全。如果需要满足线程安全,可以用 Collections.synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap

存储结构

  • JDK 7 中的 HashMap 采用数组 + 链表的结构来存储数据

  • JDK 8 中的 HashMap 采用数组+链表或树的结构存储数据

HashMap存储结构.png

核心属性

table

transient Node<K,V>[] table;

这是 HashMap 的核心,HashMap 内部实际上是一个 Node 数组,这个数组的大小必须是 2 的幂次方。
再看 Node 的实现:

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

可见,Node 实际上是单链表结构,即 HashMap 中是用单链表结构的数组来存储数据。

需要注意的是,在 JDK1.8 之后,如果单链表长度大于等于 8 ,则把单链表转换为红黑树结构。

size

transient int size;

key-value 键值对的数量,这里不是 table 数组的大小

capacity

table 数组的 length,默认是 16,这里需要注意的是,capacity 的值必须是 2 的幂次方。

loadFactor

table 能使用的比例,默认是 0.75。

当负载因子 loadFactor 增大时,每个链表所能容纳的键值对个数越多,遍历链表的时间也就越长,查询、插入等速度也就越慢,适应于内存空间较少且而时间效率要求不高的场景;

相反,当负载因子减少时,每个链表所能容纳的键值对个数越少,操作 map 的时间较快,但需要增加 table 的长度,所以需要更多的内存,适应于内存空间很多而对时间效率很高的场景;

默认值是对空间和时间效率的一个平衡选择,一般不需要修改。

threshold

int threshold;

size 需要扩容的临界值,当 size 大于等于 threshold 时,就需要进行扩容操作。

扩容后的 HashMap 容量是之前容量的两倍。

threshold = capacity * loadFactory

其中,影响 HashMap 的性能主要有两个因素:initial capacity 和 loadFactory。

方法

put

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

/*
 * hash:key 的 hash 值
 * onlyIfAbsent:如果为 true 的话,存在 value 的话不进行修改
 * evict:在 HashMap 中没有意义
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab;
    Node<K,V> p;
    int n,i;
    // 这里只有在第一次调用 put 方法时可能会触发
    if ((tab = table) == null || (n == tab.length) == 0)
        n = (tab - resize()).length;
    // 根据 key 的 hash 值找到具体位置,如果这个位置没有值,初始化一个 Node
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 获取这个位置的链表
        Node<K,V> e;
        K k;
        // 判断该位置的第一个数据和我们要插入的数据,key 是不是相等,如果是的话,取出这个节点
        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);
        // 执行到这里说明该节点属于单链表,且第一个节点 key 不相等
        else {
            for (int binCount = 0; ; ++binCount) {
                // 循环到链表的最后,插入该值
                // 在 JDK 1.8中是插入到链表的最后,JDK 1.7中是插入到最前面
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD = 8
                    // 如果新插入的值位于链表的第 8 个,则将链表转换为红黑树结构
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifBin(tab, hash);
                    break;
                }
                // 如果链表中的某个节点的 key 与要插入的 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
            // 获取这个 key 之前的 value
            V oldValue = e.value;
            // onlyIfAbsent 为 true 的话,就不要改变已有的值
            // 则这里如果 onlyIfAbsent 为 false,或 value 是 null,则将新值替换老的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // HashMap 中不进行操作
            afterNodeAccess(e);
            // 进行覆盖操作时,直接返回之前的值,说明修改值时 modCount 不修改
            return oldValue;
        }
    }
    // 如果是新增值时,需要增加 modCount变量,为迭代器服务
    ++modCount;
    // 如果数组长度大于 threshold 阈值
    if (++size > threshold)
        // 重新散列
        resize();
    // HashMap 中什么都不做
    afterNodeInsertion(evict);
    // 新增值返回 null
    return null;
}

put 函数大致可以分以下几步:

  1. 如果 table 为 null,则调用 resize() 初始化,默认是创建长度为 16 的数组;
  2. 通过与运算计算对应 hash 值的下标,如果对应下标的位置没有元素,则直接创建一个;
  3. 如果有元素,则说明该 hash 值已经有对应的节点,则再进行判断:
    1. 判断两个冲突的 key 是否相等,,如果相等的话,则到最后执行该节点的替换操作;
    2. 如果 key 不相等,再判断该节点是否为红黑树,如果是的话,则交给红黑树添加此元素;
    3. 如果只是普通链表的话,则遍历链表,判断是否有与给定 key 相等的节点,如果有的话,跳出循环,在后面执行该节点的更新操作,否则在最后添加一个节点,并且判断链表的长度是否已经大于等于 8,满足条件的话,则将链表改为红黑树。
  4. 最后,如果存在该 key 的节点,则执行更新操作
  5. 最后判断当前数组的长度是否已经大于阈值,如果大于则 rehash()

resize

resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果之前 table 的长度不为 0,则进行扩容
    if (oldCap > 0) {
        // 如果 table 长度超出最大值,则不扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 长度允许的情况进行扩容2倍
            newThr = oldThr << 1;
    } else if (oldThr > 0)
        // 这里主要是调用 new HashMap(int initialCapacity) 初始化后,第一次进行初始化的时候
        newCap = oldThr;
    else {
        // 对应调用 new HashMap() 初始化后,第一次进行初始化
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // 这里只要调用 new HashMap(int initialCapacity) 初始化,执行这里
        // 主要为了校验,newCap 是否超出最大值,且 newCap * loadFactor 是否超出最大值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    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) {
                oldTab[j] = null;
                // 如果链表只有一个,则进行直接赋值
                if (e.next == null)
                    // e.hash & (newCap - 1) 确定元素所位于的桶下标
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    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;
                        }
                    } 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 后,元素的位置要么是在原位置,要么是在原位置再移动 2次幂的位置,即原位置 + oldCap

get

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab;
    Node<K,V> first, e;
    int n;
    K k;
    // 判断 table 是否为空,如果为空则返回 null,如果不为空,根据 hash 获得桶下标
    if ((tab = table) != null && (n = tab.length) > 0 &&
       (first = tab[(n - 1) & hash]) != null) {
        // 判断该下标的桶第一个节点是否与当前 key 相等
        if (first.hash == hash &&
           ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果这个节点属于红黑树结构
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 如果该节点属于单链表,则循环该链表,获得与该 key 相等的节点
            do {
                if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

get函数可以分为下面几步:

  • 根据 key 的 hash 值找到桶所在的下标:hash & (length - 1)
  • 判断该位置的第一个元素是否是要寻找的元素,如果是,则返回
  • 判断该元素类型是否是 TreeNode,如果是,用红黑树的方式取数据
  • 遍历链表,直到找到相等的 key

小结

  • HashMap 线程不安全,如果需要在并发的情况下同时操作,建议使用 ConcurrentHashMap。
  • 负载因子默认是0.75,可以修改,并且可以大于 1,当设置其值较大时,时间效率低而空间效率高,反之则时间效率高而空间效率低,但是建议一般不要轻易修改,除非情况非常特殊。
  • 扩容是一个特别消耗性能的操作,所以在使用 HashMap 的时候,估算 map 的大小,初始化的时候给一个大致的数值,避免 map 进行频繁的扩容。
  • JDK1.8 中引入了红黑树,大程序优化了 HashMap 的性能。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容