HashMap源码解析

HashMap大家肯定非常的熟悉,HashMap底层其实就是一个数组,只不过数组的每一项都是一条链,也是面试中经常喜欢问到的知识点,"请说下HashMap的工作原理",ememem.....其实面试官主要是想考察我们知其然还要知其所以然,所以我们不能一直停留在会用api,还要带着问题去研究源码。比如,为什么HashMap的键(key)和值(value)可以为null,而HashTable键和值确却都不能为null?如果两个键的hashcode相同,会发生什么情况?以及如何取到相应的值?

为什么说要带着问题看源码呢?因为这样目的性更强效率更高,漫无目的的看源码会使你陷入其中而不能自拔,带着问题看,点到为止。下面就进入正题,首先先来一张自己画的HashMap结构草图,大家将就着看吧,有助于理解
hashmap数据结构图.png

简单的说明下,HashMap底层其实就是一个table数组,只不过数组的每一项是由一条链组成的,而这每一条链表都是由若干个节点组成,每一个节点就是一个Entry对象,每一个Entry对象中存储的是我们的key对象和value对象以及下一个节点next(链表中如果不止一个节点的话,那么前一个节点的next就会指向下一个节点),由上图可知,假如有一条很长很长的链表分布在数组的某一项上面,如果我们想取某一个value值的话,就需要遍历数组i位置的这条链表,所以如果这些链表都能均匀的分布到数组的每一项上,而不是像刚才那样的话,我们取值的速度就会快很多。其实这些在HashMap内部都做了很好的优化处理,接下来我们就一起来探究HashMap源码,看看是如何优化的。

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

可以看到HashMap继承自AbstractMap抽象类,实现了Map接口中的方法
接着看下HashMap的一些成员变量声明

//默认的初始容量(必须是2的整数次幂),jdk版本不同,有的是16
static final int DEFAULT_INITIAL_CAPACITY = 4;
//最大容量(2的30次方),如果配置的容量大于此值的话就会用此值替换
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子(0.75是基于时间和空间的一种折中结果),在扩容时起到关键作用
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空table数组(HashMap的底层其实就是一个链式数组)
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
//存储数据的HashMapEntry数组(静态内部类HashMapEntry实现了Map.Entry<K,V>接口)
//table数组中存储的就是一个个Entry链,Entry中存储的就是一个个key-value键值对数据
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
//键值对的数量
transient int size;
//HashMap的阈值,用于判断是否需要扩容(threshold = 容量*加载因子)
int threshold;
//加载因子实际大小
final float loadFactor = DEFAULT_LOAD_FACTOR;
//HashMap修改的次数
transient int modCount;

通常我们用到HashMap的时候需要去new HashMap();那么我们就从HashMap的构造方法开始查看

//传入容量值、加载因子
public HashMap(int initialCapacity, float loadFactor) {
        //容量值不能小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //如果设置的容量值大于HashMap允许的最大容量,纠正并将HashMap允许的最大值赋值给他
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            //同理如果小于HashMap的默认初始容量大小的话,就将默认值赋值给他
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }
        //加载因子不能小于0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // Android-Note: We always use the default load factor of 0.75f.

        // This might appear wrong but it's just awkward design. We always call
        // inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
        // to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
        // the load factor).
        //在部分版本上这个阈值等于容量*加载因子
        threshold = initialCapacity;
        init();
    }

//一个参的构造方法,加载因子用HashMap默认的
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//无参的构造方法
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

//将子map传进来,如果m为null的话会报空指针错误
public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        //创建链表数组
        inflateTable(threshold);
        //将m中的key-value键值对都添加到HashMap数组中
        putAllForCreate(m);
    }

这里要特别说明的是,HashMap链表数组中存储的是键值对(key对象和value对象),而不是只存储value或者key

接下来分析下HashMap是如何存取的

public V put(K key, V value) {
        //先判断当前链表数组是否为空数组,是则调用inflateTable方法创建一个数组
        if (table == EMPTY_TABLE) {
            //此方法中通过table = new HashMapEntry[capacity]创建一个数组
            inflateTable(threshold);
        }
        //如果key是null的话,会调用putForNullKey方法,将值存放到table数组的第0项位置处,即第一个桶中
        if (key == null)
            return putForNullKey(value);
        //根据key的hashcode计算出hash值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //通过hash值计算出此条键值对在桶中的位置
        int i = indexFor(hash, table.length);
        //HashMapEntry是单链表结构,e.next查找下一个节点数据
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //遍历,如果key相同,那么就将此key的新value值覆盖旧value值,并直接返回旧value值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //走到这一步说明添加到数组中的键值对的key不相同,那么就将此键值对数据添加到数组的i位置
        addEntry(hash, key, value, i);
        return null;
    }

通过对key==null的处理,我们看下putForNullKey(value)方法

//从此方法中我们可以得到两点关键信息
private V putForNullKey(V value) {
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            //1、由下面第二点注释可知,key为null的键值对数据是都存储在table[0]位置的
            //2、程序能够进入这个if (e.key == null)判断中,说明table[0]位置已经存在了key为null的键值对数据
            //所以,遍历table[0]如果此位置已经有key为null的value数据了,那么就将新value值覆盖旧value值,并将旧value值返回
            //同时还能得出HashMap的key是唯一的,相同的key对应的键值对会被覆盖
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //2、如果key为null的话,并且之前table[0]位置还没有存储key为null的键值对数据时,会将此键值对添加到table[0]位置
        addEntry(0, null, value, 0);
        return null;
    }

接下来我们再看下如果我们在存储时key为null和key不为null两种情况下是如何将这个键值对存储到链表数组中的

//key为null的情况,并且是第一次存储时
//由上面分析已经知道key=null时位于table[0]位置,所以bucketIndex为0,他的hash值也为0
addEntry(0, null, value, 0);
//要存储的key不为null的情况
addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
        //首先在put存储key-value对象时,要判断当前数组中存储的key-value对象数量是否达到了HashMap的阈值,
        //超过阈值则需要对HashMap进行扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //对HashMap进行扩容,重新调整他的容量
            resize(2 * table.length);
            //key为null的话那么hash值为0;
            //key不为null的话,由于扩容了,所以需要重新根据key的hashcode值去计算hash值
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            //通过hash值计算出此条key-value对象在table数组中的位置索引
            //因为进行了扩容,所以需要重新确定bucketIndex 
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

HashMap的扩容

接着我们看下resize方法

void resize(int newCapacity) {
        //旧HashMap
        HashMapEntry[] oldTable = table;
        //旧HashMap的容量
        int oldCapacity = oldTable.length;
        //在扩容前需要进行判断,
        //如果旧HashMap的容量已经达到最大要求值,则不能再扩容,直接返回,同时将阈值设置为Integer.MAX_VALUE
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //根据扩容要求的容量大小new一个新的HashMap
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        //下面会介绍
        transfer(newTable);
        //并将新HashMap赋值给旧HashMap
        table = newTable;
        //阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

看下这个transfer方法

void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        //主要操作就是循环遍历旧HashMap,取出一个个key-value对象并存储到扩容之后的新HashMap中
        //由此可见扩容是耗性能的,所以如果我们知道要存储的数量大致数量的话,在new这个HashMap的时候就给他一个合适的容量,
        //这样也就避免了扩容,降低了性能的损耗
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

上面我们分析了HashMap是如何进行扩容的,以及扩容需要注意和优化的知识点,接下来继续看createEntry方法

void createEntry(int hash, K key, V value, int bucketIndex) {
        //将table数组的bucketIndex位置的值赋给e链表
        HashMapEntry<K,V> e = table[bucketIndex];
        //1、将key-value键值对数据插入到新new的Entry链表中
        //2、再将这个存储了新键值对的HashMapEntry存储到table数组的bucketIndex处
        //3、设置e指向下一个节点(新插入的键值对都是存储在链表的头部的)
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        //put一条键值对数据之后,键值对数量就+1
        size++;
    }

public V get(Object key) {
        //取key为null的value值,上面分析已经知道,key为null时,key-value是存储在table[0]位置
       //getForNullKey()方法就是循环遍历table[0]位置找到key为null时对应的value值
        if (key == null)
            return getForNullKey();
        //key不为null时
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

下面把getForNullKey()方法和getEntry(key)方法也贴出来分析下

//这个方法在上面取值方法中已经简单分析过了
private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }


final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //日式走到这个方法时,key是不为null的,不明白这里为什么还要对key是否为null进行判断??
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //根据key的hashcode计算出hash值,再根据hash值就可以计算出在table数组中的哪一个位置
       //得到table在这个位置处的HashMapEntry链表,循环遍历链表中的节点
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

到此,对于HashMap如何存储、扩容、以及取值等进行了详细的分析,基于jdk版本的不同,可能源码也不相同,例如,最新的HashMap源码中是通过树来进行存储的,我也是分析完之后发现并不是最新的源码,所以,以后有时间再对最新的HashMap源码进行分析吧,如有分析不对的地方,还望各位老铁指正

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

推荐阅读更多精彩内容