从源码的角度来谈一谈HashMap的内部实现原理

HashMap能够说是咱们一个熟悉又陌生的Java中经常使用的存储数据的API。说他熟悉,是由于咱们常常使用他,而说他陌生是由于咱们大部分时间是只知道他的使用,而并不知道他内部的原理,可是在面试考察的时候又最喜欢去问这个原理。今天,我就来从源码的角度,谈谈对HashMap的理解。面试

HashMap概述

hashMap的底层实际上是基于一个数组来进行数据的存储和取出。他继承于Map这个接口来实现,经过put和get方法来操做数据的存和取。具体对于hashMap的使用,这里不在具体举例说明,使用起来并不困难。不过在谈到HashMap的内部原理以前,咱们须要了解一下几个名称的意思。数组

1.initialCapacity。 这个翻译为初始化容量。为hashMap的存储的初始化空间的大小,咱们能够经过构造方法来指定其大小,也能够不指定采用 默认大小16。这里须要说明一下,通常来讲,容器的大小为2的幂次方。至于为何会是2的幂次方,具体缘由能够参考这篇文章。为何hashmap的初始化大小为2的幂次方bash

2.loadFactor。 加载因子。当hashmap的存储容量达到了必定上限以后,若是还须要进行数据的存储,则会利用加载因子对其进行扩容操做。通常而言,扩容大小为如今容量的0.75倍。举个例子,假设如今的hashMap的初始化大小为16,可是如今因为容量已满又要插入新的元素,因此先进行扩容操做,将容量扩充为16*0.75=12,也就是说扩大了12个容量。源码分析

3.threadshold: 扩容阀值。即扩容阀值 = HashMap总容量*加载因子。当hashMap的容量大于或者等于扩容阀值的时候就会去执行扩容。扩容的容量为当前HashMap总容量的两倍。ui

这里有一张网上找来的图,来讲明hashMap内部存储原理。this

hashMap

源码解析

咱们在使用hashMap的时候,通常来讲都是用put和get方法,因此咱们分析源码,就从这两个方法着手分析内部原理。spa

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<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;
    }

复制代码

咱们先来看看put方法。从代码能够看出,put方法主要作了这么几件事。.net

1.当咱们在将key和value添加进入hashMap的时候,首先其会去判断table是否为空(EMPTY_TABLE)。这里须要说明下,这个table实际上是一个数组,咱们前面提到过,hashmap内部实际上是一个数组来对数据进行存储,因此这个table其实能够写成table[ ]。当判断这个table数组为空的时候,他会去调用infalteTable()方法。而这个方法是作什么的呐,咱们在跳进去看看。翻译

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
 
        // Android-changed: Replace usage of Math.min() here because this method is
        // called from the <clinit> of runtime, at which point the native libraries
        // needed by Float.* might not be loaded.
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }
 
        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];
    }

复制代码

能够看到,其实这个inflateTable方法是在对hashmap进行初始化容量操做。其初始化容量为capacity * loadFacctor。也就是咱们前面说过的 初始化容量 * 加载因子。code

2.以后hashmap回去判断你储存的key是否为空,if(key == null),若是为空,则调用putForNullKey()方法来进行空key的操做。这里能够说是hashMap与hashTable的一个最大不一样的地方,hashMap容许key为空,他有相应的处理key为空的操做方法,可是hashTable却不能容许key为空,他没有相应的操做方法。

3.以后对key进行一次hashcode的计算而且计算其index。紧接着遍历整个table数组,判断是否有相同的key,若是发现有相同的key,则将key所携带的新的value替换掉以前旧的value,从而确保key的惟一性。以后进行addEntry方法中。

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
 
        createEntry(hash, key, value, bucketIndex);
    }

复制代码

咱们进入到addEntry方法中查看。发现里面会先对数组须要存储的大小和阀值进行一次比较,若是发现要存储的已经超过了threshold阀值,那么就要调用resize对其进行扩容操做。扩容的小大为2*table.length。以后重新计算hash,将结果存储到bucket桶里面。

那么resize()方法中又作了那些操做呐?

void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

复制代码

咱们能够看到resize里面仅仅只是初始化了一个新的更大的table数组,而且把老的数据重新添加进入了新的table里面去。

最后咱们回到creatEntry方法中,查看发现若是在bucket桶内发生了hash的碰撞,则将其转化为链表的形式来进行存储,不过在Java1.8以后会将其变为红黑树的形式存储。在此将put方法源码分析完成。

咱们再来看下get()方法。

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
 
        return null == entry ? null : entry.getValue();
    }

复制代码

get方法一开始和put相似,都是先判断key是否为空,若是为空,则调用相应的getForNullKey方法去进行处理。不为空,调用getEntry去进行查找。咱们再来看看getEntry里面又作了什么操做。

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
 
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        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;
    }

复制代码

咱们能够看到,里面也是先对key进行了一次hash操做,以后经过这个hash值来进行查找,若是发现hash值相等,则再经过比较key的值来进行查找,最终找到咱们想要的e将其return返回,否则则返回为空,表明找不到此元素。

到此hashMap的总体原理讲解完毕。