HashMap源码解析

本文主要内容

  • Hash原理
  • 存储过程
  • 读取过程
  • 总结

各种容器类、线程池等常见Java知识,经常在面试的过程中被问起,工作中也经常被提及,比如为啥说Map的查找效率近似为1。为此,本人接下来会专门总结这类知识。

Hash原理

Object类有本身就有hash方法,但HashMap类中对key的hash值做了专门处理:

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

h是key的原来hash值,map的查找比较有意思,效率非常高,通过计算key的hash值,来确定value的位置,知道位置就能直接读取数组返回对应的value值了。

static int indexFor(int h, int length) {
    return h & (length-1);
}

key的hash值取值范围非常宽,基本就是int的最大值与最小值这么分布的,非常的松散,那为什么HashMap还要专门处理下key的hash值呢?

从indexFor方法中能看到,key的hash值与(length-1)相与之后,基本只能保留很小的一部分,如果当前HashMap长度为16,那么相与后,key原本的hash值只能保留最后4位,这样会导致哈希碰撞特别多,影响整体效率。例如下图:

在JDK7中,key的hash值会做4次异或操作,在JDK8中,目前只用做一次异或操作了,异或操作的目的在于:

混合原始哈希码的高位和低位,以此来加大低位的随机性,而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

有数据表明,这么做之后哈希碰撞的概率能减少10%

存储过程

HashMap其实是使用数组进行存储的,但它的数组长度是特定的,只能是2的指数。看看HashMap的初始化方法:

public HashMap(int initialCapacity, float loadFactor) {
    // 略过初始长度异常的处理
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

initialCapacity即是用户传入的初始Map长度,capacity 是真实的长度,capacity 初始值为1,当capacity 小于initialCapacity时,则 capacity 向右移1位,即是乘以2,整个过程循环,最终capacity 的值只会是2的指数次幂。

再来查看存储方法:

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    //获取hash值
    int hash = hash(key.hashCode());
    //计算索引位置
    int i = indexFor(hash, table.length);
    //如果索引位置已经有元素了
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

HashMap使用数组来存储数据,数组中元素类型是Entry类型。

    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

Entry中有4个成员变量,其中一个是next,我们很容易想到,这可以形成一个链表,所以HashMap其实是使用数组存储数据,如果发生哈希碰撞,那么索引处会形成一个链表。

如果在插入位置已经有一个值了,如果key和hash都一模一样,则直接替换成新值即可,如果仅是位置一样呢?

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

  Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

在addEntry方法中,先取得这个位置上原来的值,然后将数组的这个位置赋值为新的key和value,比较巧妙的是Entry的构造方法,数组新元素的next值即是老的元素e,这样链表也更新的。链表是一直在更新head元素,代码看起来更加简洁。

如果在存储的过程中发现长度不够,则需要扩展数组的长度为先前长度的2倍,这也从侧面印证了HashMap的长度是2的指数幂。

  void resize(int newCapacity) {
    Entry[] oldTable = table;
    Entry[] newTable = new Entry[newCapacity];
    //transfer方法将之前数组中的值转移过来
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

因为元素在HashMap中的索引,是由hash和数组长度与计算得到的,所以在元素的转移过程当中,原有元素的位置很可能发生了变化

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                //重新计算新的索引值
                int i = indexFor(e.hash, newCapacity);
                //典型的链表赋值
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

在数据转移过程中,有趣的是链表值的转移,能够推测出新链表中head值为原来链表中的tail值了,因为newTable[i]的值一直在被重置为链表后边的值。

读取过程

  public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

读取过程则比较简单了,根据hash查到索引值,如果索引处存在链表,则遍历链表,如果不存在则直接返回了。

总结

HashMap的原理还是比较简单,最复杂的是对hash值的处理,还是文中对链表的操作,理解了这两部分就能很清晰地理解HashMap了。

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

推荐阅读更多精彩内容

  • 前言 今天来介绍下HashMap,之前的List,讲了ArrayList、LinkedList,就前两者而言,反映...
    嘟爷MD阅读 2,871评论 2 56
  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,664评论 9 107
  • 淘气娃小时候是生长在老家的乡村小学。 淘气娃的淘气事可不少,先说上几件吧! 记得她两岁那年的一天,房间内...
    三丫头小美金阅读 660评论 0 1
  • 今晚终于闲下心来写一写,可能会写的比较乱,还请多多包涵。 今天是大年初一,可我却一点也没有感受到春节的气息,也许是...
    仇心舞落雪阅读 406评论 0 2
  • 正月里暖热炕 读写人家作者:白峰 正月里正月正,正月里来是新年,新年大家很休闲,走亲戚,串朋友,逛街道,进公园,在...
    读写人家阅读 585评论 0 1