本文针对HashMap
的源码分析基于JDK 7
,JDK 8
在HashMap
的实现上有着较大幅度的改进和优化,这部分优化我将另起一篇来阐述。另外,本文仅分析HashMap
众多方法中最经常使用的方法,其他方法有须要时再研究 。html
HashMap
的继承关系以下。java
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap
继承自AbstractMap
,同时实现了Map
、Cloneable
和Serializable
接口。所以,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
),经过计算Key
的hash
值来决定存入哪一个数组单元(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
键值对的数量。
threshold
是HashMap
决定是否执行执行扩容操做的阈值,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_CAPACITY
和HashMap
构造方法时强调的HashMap
的bucket
容量必须是2
的幂。当length
是2
的幂,那么length
的二进制数能够表示为1000...000
,所以length - 1
的二进制数为0111...111
,当h
与length - 1
位与时,除了h
的最高位的被修改成0
,其他位均保持不变,这也正是实现了h % length
的效果。只是相比于h % length
,h & (length-1)
的效率会更高。
HashMap
的bucket
容量必须为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
元素时,先计算Key
的hash
值,定位到数组中对应的bucket
,而后开始遍历Entry
单链表,直到找到须要的元素,不然返回null
。
当咱们向HashMap
中put
新的键值对时,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
过程当中出现线程不安全问题的根源。
HashTable
和HashMap
底层采用相同的存储结构,在不少方法的实现上两者的思路基本一致。最主要的区别主要有两点。
HashTable
实现了所谓的线程安全,在HashTable
不少方法上都加上了synchronized
。
在HashMap
的分析中,咱们发现当咱们新增键值对时,HashMap
是容许Key
和Value
均为null
。可是HashTable
不容许Key
或Value
为null
,关于这一点咱们能够经过查看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; }