HashMap源码分析

HashMap简介

本文针对HashMap的源码分析基于JDK 7JDK 8HashMap的实现上有着较大幅度的改进和优化,这部分优化我将另起一篇来阐述。另外,本文仅分析HashMap众多方法中最经常使用的方法,其他方法有须要时再研究 html

HashMap的继承关系以下。java

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

HashMap继承自AbstractMap,同时实现了MapCloneableSerializable接口。所以,HashMap能够被克隆,并支持序列化。另外,HashMap是一个非线程安全的,所以适合运用在单线程环境下。若是是在多线程环境,能够经过Collections的静态方法synchronizedMap得到线程安全的HashMap,以下代码所示。shell

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

存储结构

针对每一个键值对,HashMap使用内部类Entry来存储,Entry核心代码以下。数组

static class Entry<K, V> implements Map.Entry<K, V> {
    final K key;
    V value;
    Entry<K, V> next;
    final int hash;
  
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
}

从总体上看,HashMap底层的存储结构是基于数组和链表实现的。对于每个要存入HashMap的键值对(Key-Value Pair),经过计算Keyhash值来决定存入哪一个数组单元(bucket),为了处理hash冲突,每一个数组单元其实是一条Entry单链表的头结点,其后引伸出一条单链表。HashMap的存储结构以下图所示。安全

图片描述

关键属性

HashMap定义了几个关键属性,对应的源码以下。多线程

static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
  • DEFAULT_INITIAL_CAPACITY表明HashMap槽(bucket)的默认容量,且该容量必须为2的幂,具体缘由会在下文解释。源码分析

  • MAXIMUM_CAPACITY表明HashMap槽(bucket)的最大容量,若是传入的容量大于1 << 30,那么实际容量会被MAXIMUM_CAPACITY替换。优化

  • DEFAULT_LOAD_FACTOR是默认的加载因子,用于计算HashMap扩容的threshold,当HashMap的实际元素容量达到总容量的threshold时,对HashMap进行扩容。this

  • table是存储Entry的数组,每一个Entry是一条单链表的头结点。spa

  • size表明HashMap键值对的数量。

  • thresholdHashMap决定是否执行执行扩容操做的阈值,threshold = capacity * load factor

  • loadFactor表示HashMap实际加载因子,经过构造方法传入。若未指定,loadFactor等于DEFAULT_LOAD_FACTOR

须要进一步解释的是loadFactor属性,loadFactor描述了HashMap发生扩容时的填充程度。若是loadFactor设置过大,意味着在HashMap扩容前发生hash冲突的机会越大,所以单链表的长度也就会越长,那么在执行查找操做时,会因为单链表长度过长致使查找的效率下降。若是loadFactor设置太小,那么HashMap的空间利用率会下降,致使HashMap在不少空间都没有被利用的状况下便开始扩容。

构造方法

HashMap定义了四个构造方法,源码以下。

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

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

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init(); // 在源码中,init方法体不执行任何操做。
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

当调用HashMap默认构造方法时,HashMap对象的属性均会被设置为默认值,包括设置加载因子(DEFAULT_LOAD_FACTOR)、扩容阈值(threshold)和table的初始大小。

若是在建立HashMap对象时指定了bucket容量initialCapacity,经过源码咱们能够看出在初始化对象时不必定会直接使用initialCapacity,而是选取知足小于等于initialCapacity前提条件下最大的且是2的幂的一个值做为实际bucket的大小。

若是向构造方法传递的参数是一个Map对象m,那么putAllForCreate方法会从新散列m中的每一个元素,将它们存入相应的bucket中。putAllForCreate方法及其调用的相关方法以下。

private void putForCreate(K key, V value) {
        int hash = (key == null) ? 0 : 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 != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }

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

    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

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

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

putAllForCreate方法遍历每个键值对e,经过putForCreat方法将e散列到对应的bucket中。putForCreate方法调用indexFor来肯定键值对散列的bucket的位置。indexFor经过h & (length-1)返回bucket的位置,接着遍历对应的单链表来决定是更新操做仍是插入操做。

咱们须要关注的地方是indexFor为何经过计算h & (length-1)来得到bucket的位置,而不是经过计算h % length

实际上,在HashMap中,h & (length-1) == h % length,可是须要一个前提:length必须知足是2的幂。这也正是在解释DEFAULT_INITIAL_CAPACITYHashMap构造方法时强调的HashMapbucket容量必须是2的幂。当length2的幂,那么length的二进制数能够表示为1000...000,所以length - 1的二进制数为0111...111,当hlength - 1位与时,除了h的最高位的被修改成0,其他位均保持不变,这也正是实现了h % length的效果。只是相比于h % lengthh & (length-1)的效率会更高。

HashMapbucket容量必须为2的幂的另外一个重要缘由是一旦知足此条件,那么length即为偶数,length - 1便为奇数,因此length - 1的最后一位必为1。所以,h & (length - 1)获得的值既多是奇数,也多是偶数,这确保了散列的均匀性。若是length - 1是偶数,那么h & (length - 1)获得的值必为偶数,那么HashMap的空间便浪费了一半。

存取方法

咱们分析HashMap使用频率最高的两个方法get方法和put方法,源码以下。

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

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        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获取get元素时,先计算Keyhash值,定位到数组中对应的bucket,而后开始遍历Entry单链表,直到找到须要的元素,不然返回null

当咱们向HashMapput新的键值对时,HashMap首先检查Key是否等于null,若为null,则执行putForNullKey方法,putForNullKey方法对应的源码以下。

private V putForNullKey(V value) {
        for (Entry<K,V> 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;
    }

若是Key等于null,那么就将该键值对添加到table[0]的位置,同时,遍历table[0]处的单链表并将链表中全部节点的值都覆盖为新传递进来的键值对的值。所以,该位置永远只有一个值。

若是Key不等于null,那么经过indexFor定位到bucket,而后遍历单链表,若是存在Key相等的键值对,就用新值覆盖旧值,并返回旧值。若是在单链表中没有找到对应的Key,那么调用addEntry方法建立新的Entry节点至单链表(做为头节点)。addEntry及关联方法源码以下。

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

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

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

addEntry把新增键值对插入单链表后,会判断是否须要扩容,即判断当前HashMap的元素的个数是否大于threshold。若须要扩容,那么调用resize方法进行2倍扩容。resize方法会在内部调用transfer方法,transfer方法遍历旧数组及单链表,并将每一个键值对从新散列,能够意识到,这整个rehash的开销至关大。

线程安全

关于线程安全,咱们想要知道的是HashMap在什么状况下会发生线程不安全的状况?实际上,在上文分析put方法时,当HashMap的容量超过了threshold时,便执行resize操做,resize就存在线程不安全的问题。

关于resize哪儿不安全,我推荐左耳朵耗子写的疫苗:Java HashMap的死循环,这篇文章图文并茂的解释了在rehash过程当中出现线程不安全问题的根源。

HashMap VS HashTable

HashTableHashMap底层采用相同的存储结构,在不少方法的实现上两者的思路基本一致。最主要的区别主要有两点。

  • HashTable实现了所谓的线程安全,在HashTable不少方法上都加上了synchronized

  • HashMap的分析中,咱们发现当咱们新增键值对时,HashMap是容许KeyValue均为null。可是HashTable不容许KeyValuenull,关于这一点咱们能够经过查看HashTable源码得知。

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) { // 若value为空则抛出NullPointerException。
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode(); // 若key为空则抛出NullPointerException。
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }
相关文章
相关标签/搜索