HashMap 源码解析

HashMap 是什么?

在我们的印象里,HashMap是一个存储一对一关系的数据结构,
采用hash算法,所以读取和存储效率较高。

问题

  1. hash算法是如何实现的?

源码

注释

从类的注释我们可以得到以下内容

  1. HashMap是实现了Map接口的Hash Table
  2. HashMap几乎和HashTable一样,除了非线程安全和允许null key/value
  3. HashMap不保证数据顺序保持不变
  4. 常规操作保证稳定的效率(如get put)
  5. 遍历效率取决于Hash Table的大小和数据的大小
  6. HashMap的效率取决于初始容量和加载因数(加载因数表示数据量达到Hash Table容量的多少时,需要扩容)
  7. 扩容时,会重新排列数据(rehash)
  8. 初始加载因数是0.75,初始容量请根据实际需要设置(避免过多的扩容或者浪费空间)
  9. HashMap非线程安全,修改map的结构时,遍历会抛出异常

可以看到注释基本写的很全,会把我们关心的东西说清楚

实现

总结来说如下

  1. 通过自定义的Node存储Key和Value,记录一个对应关系
  2. 通过Node数组存储所有对应关系,数组的下标由key的hashcode和数组大小计算得出
  3. 如果数组下标相同,通过单链表将相同下标的Node保存起来
  4. 如果单链表的Node个数超过一定阈值,改为红黑树存储
  5. 如果Node的总个数超过一定阈值,扩容,重新排列Node

简单来看下get和put的实现

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 可以看到 下标的计算是 数组大小&hash
            // 如果对应下标处为null,就直接存储
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p; // 对应下标第一个Node就是要修改的Node
            else if (p instanceof TreeNode)
                // 如果是以红黑树的形式存储,在树中找到插入或者修改的Node
                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);
                        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))))
                        // 找到要修改的Node
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                // 修改找到的Node
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // modCount表示数据修改了
        ++modCount;
        if (++size > threshold)
            // 个数超出阈值,扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }


    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;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                // 判断数组中此下标存的第一个是不是要找的Node
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    // 红黑树保存,则从树中查找
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 单链表存储,则变量查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

总结

对于开发使用来说,需要有以下几点注意

  1. 非线程安全, 应该用封装HashMap的数据结构实现线程安全,或者使用Map m = Collections.synchronizedMap(new HashMap(...));创建线程安全的Map
  2. 初始大小根据具体使用情况决定,避免过多性能消耗,也不能设置的太大浪费空间
  3. HashMap遍历的顺序是不稳定的,可能根据数据的增多而变化

备注

此文中源码来源于 Android-28 SDK

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

推荐阅读更多精彩内容