HashMap源码浅析 Base 1.7

549 阅读8分钟

JDK版本1.7

数据结构
底层实现
初始容量
扩容
负载因子(默认值)
线程安全
链表插入值方法
HashMap
数组+链表
16
整个map扩容,newsize = oldsize*2,size一定为2的n次幂
0.75
头插法

HaspMap

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)

源码解析

1.初识HashMap实现

public class HashMap
    extends AbstractMap
    implements Map, Cloneable, Serializable
{

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  初始化桶大小,因为底层是数组,所以这是数组默认的大小

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30; //桶的最大值

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的负载因子(0.75)

    /**
     * An empty table instance to share when the table is not inflated.
     */
    static final Entry[] EMPTY_TABLE = {};

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table = (Entry[]) EMPTY_TABLE; //table 真正存放数据的数组

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size; //Map 存放数量的大小

    /**
     * The next size value at which to resize (capacity * load factor).
     * @serial
     */
    // If table == EMPTY_TABLE then this is the initial capacity at which the
    // table will be created when inflated.
    int threshold; //桶大小,可在初始化时显式指定

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor; //负载因子,可在初始化时显式指定

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * The default threshold of map capacity above which alternative hashing is
     * used for String keys. Alternative hashing reduces the incidence of
     * collisions due to weak hash code calculation for String keys.
     * 
     * This value may be overridden by defining the system property
     * {@code jdk.map.althashing.threshold}. A property value of {@code 1}
     * forces alternative hashing to be used at all times whereas
     * {@code -1} value ensures that alternative hashing is never used.
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
通过以上代码块中可以看出相关属性及定义,重点解释下“负载因子”:用于计算Entry数组临界值threshold,通过capacity * loadFactor计算的得出数组临界值。
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

HashMap中给定的默认容量为 16,负载因子为 0.75,Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能,因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。

根据代码可以看到其实真正存放数据的是
`transient Entry[] table = (Entry[]) EMPTY_TABLE;`
这个Entry数组,它又是如何定义的呢?
static class Entry implements Map.Entry {
        final K key;
        V value;
        Entry next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:
  1. key 就是写入时的键

  2. value 自然就是值

  3. HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构

  4. hash 存放的是当前 key 的 hashcode

2.put方法

2-1 put方法一览
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {  //判断当前数组是否需要初始化
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);  //如果 key 为空,则 put 一个空值进去
        int hash = hash(key);  //根据 key 计算出 hashcode
        int i = indexFor(hash, table.length);  //根据计算出的 hashcode 定位出所在桶
        for (Entry e = table[i]; e != null; e = e.next) {  //如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值
            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);  //如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置
        return null;
    }
  • 判断当前数组是否需要初始化
  • 如果 key 为空,则 put 一个空值进去
  • 根据 key 计算出 hashcode
  • 根据计算出的 hashcode 定位出所在桶
  • 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值
  • 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置
2-2 hash方法、indexFor方法一览

当向 HashMap 中 put一对键值时,它会根据 key的 hashCode 值计算出一个位置, 该位置就是此对象准备往数组中存放的位置。计算过程参看如下代码:

transient int hashSeed = 0;
final int hash(Object k) {
     int h = hashSeed;
     if (0 != h && k instanceof String) {
         return sun.misc.Hashing.stringHash32((String) k);
     }

     h ^= k.hashCode();

     // 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);
 }

 /**
  * Returns index for hash code h.
  */
 static int indexFor(int h, int length) {
     // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
     return h & (length-1);
 }

通过hash计算出来的值将会使用indexFor方法找到它应该所在的table下标。当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表(拉链法)。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next(这里要注意的是,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上)。即将新值作为此链表的头节点,为什么要这样操作?据说后插入的Entry被查找的可能性更大(因为get查询的时候会遍历整个链表)。有一种说法就是链表查找复杂度高,可插入和删除性能高,如果将新值插在末尾,就需要先经过一轮遍历,这个时间复杂度高,开销大,如果是插在头结点,省去了遍历的开销,还发挥了链表插入性能高的优势。

如果该位置没有对象存在,就将此对象直接放进数组当中;如果该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(为了判断是否值相同,map不允许键值对重复), 如果此链上有对象的话,再去使用 equals方法进行比较,如果对此链上的每个对象的 equals 方法比较都为 false,则将该对象放到数组当中,然后将数组中该位置以前存在的那个对象链接到此对象的后面。
2-3 addEntry、createEntry方法一览

找到数组下标后,会先进行key判重,如果没有重复,就准备将新值放入到链表的表头。

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩容
        resize(2 * table.length);
        // 扩容以后,重新计算 hash 值
        hash = (null != key) ? hash(key) : 0;
        // 重新计算扩容后的新的下标
        bucketIndex = indexFor(hash, table.length);
    }
    // 往下看
    createEntry(hash, key, value, bucketIndex);
}
// 这个很简单,其实就是将新值放到链表的表头,然后 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
这个方法的主要逻辑就是先判断是否需要扩容,需要带的话先扩容,然后再将这个新的数据插入到扩容后的数组的相应位置处的链表的表头。
扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中。由于是双倍扩容,迁移过程中,会将原来table[i]中的链表的所有节点,分拆到新的数组的newTable[i]和newTable[i+oldLength]位置上。如原来数组长度是16,那么扩容后,原来table[0]处的链表中的所有元素会被分配到新数组中newTable[0]和newTable[16]这两个位置。扩容期间,由于会新建一个新的空数组,并且用旧的项填充到这个新的数组中去。所以,在这个填充的过程中,如果有线程获取值,很可能会取到 null 值,而不是我们所希望的、原来添加的值。
而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。
2-4 putForNullKey方法一览
前面说过HashMap的key是允许为null的,当出现这种情况时,这个会放到table[0]中。
private V putForNullKey(V value) {
    for (Entry e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

当size>=threshold( threshold等于“容量  * 负载因子”)时,会发生扩容。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {  //扩容需满足这两个条件
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}
  • 特别提示:jdk1.7中resize,只有当 size>=threshold并且 table中的那个槽中已经有Entry时,才会发生resize。即有可能虽然size>=threshold,但是必须等到相应的槽至少有一个Entry时,才会扩容,可以通过上面的代码看到每次resize都会扩大一倍容量(2 * table.length)。

3.get方法

3-1 get方法一览
public V get(Object key) {
        if (key == null)   //判断key是否为空
            return getForNullKey();   //key为空则直接返回table[0]
        Entry entry = getEntry(key);

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

    final Entry getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);   //根据 key 计算出 hashcode,然后定位到具体的桶中
        for (Entry 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))))  //key、key 的 hashcode 是否相等来返回值
                return e;
        }
        return null;
    }
  • 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中
  • 判断该位置是否为链表
  • 不是链表就根据 key、key 的 hashcode 是否相等来返回值
  • 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值
  • 啥都没取到就直接返回 null