一:定义
HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map。
public class HashMap<K,V> extends AbstractMap<K,V> 2 implements Map<K,V>, Cloneable, Serializable
二:构造函数和数据结构
HashMap提供了三个构造函数:
1 HashMap()
2
3 HashMap(int initialCapacity)
4
5 HashMap(int initialCapacity, float loadFactor)
第一个:构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
第二个:构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
第三个:构造一个带指定初始容量和加载因子的空 HashMap。
那么这两个参数有什么含义呢?在HashMap中有什么作用?我们先来看一下HashMap的数据结构深入理解之后在回过头来看。我们看每次调用map.put方法是,其实我们是往Entry<K,V>[] tab 这个数组里面添加元素的。那么Entry这个又是一个什么呢?
1 static class Entry<K,V> implements Map.Entry<K,V> {
2 final K key;
3 V value;
4 Entry<K,V> next;
5 final int hash;
6
7 /**
8 * Creates new entry.
9 */
10 Entry(int h, K k, V v, Entry<K,V> n) {
11 value = v;
12 next = n;
13 key = k;
14 hash = h;
15 }
16 }
很显然,这其实是一个链表的数据结构。所以我们分析一下,那么HashMap的数据结构是不是就是张的这个样子的呀?
其实呢,我们在开始的构造函数里面的initialCapacity这个参数,就是指的这个数组的长度了,我们在看一下具有两个参数的那个构造方法。
1 public HashMap(int initialCapacity, float loadFactor) {
2 //初始容量不能<0
3 if (initialCapacity < 0)
4 throw new IllegalArgumentException("Illegal initial capacity: "
5 + initialCapacity);
6 //初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
7 if (initialCapacity > MAXIMUM_CAPACITY)
8 initialCapacity = MAXIMUM_CAPACITY;
9 //负载因子不能 < 0
10 if (loadFactor <= 0 || Float.isNaN(loadFactor))
11 throw new IllegalArgumentException("Illegal load factor: "
12 + loadFactor);
13
14 // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
15 int capacity = 1;
16 while (capacity < initialCapacity)
17 capacity <<= 1;
18
19 this.loadFactor = loadFactor;
20 //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
21 threshold = (int) (capacity * loadFactor);
22 //初始化table数组
23 table = new Entry[capacity];
24 init();
25 }
每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。
三,添加数据
1 public V put(K key, V value) {
2 //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
3 if (key == null)
4 return putForNullKey(value);
5 //计算key的hash值
6 int hash = hash(key.hashCode()); ------(1)
7 //计算key hash 值在 table 数组中的位置
8 int i = indexFor(hash, table.length); ------(2)
9 //从i出开始迭代 e,找到 key 保存的位置
10 for (Entry<K, V> e = table[i]; e != null; e = e.next) {
11 Object k;
12 //判断该条链上是否有hash值相同的(key相同)
13 //若存在相同,则直接覆盖value,返回旧value
14 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
15 V oldValue = e.value; //旧值 = 新值
16 e.value = value;
17 e.recordAccess(this);
18 return oldValue; //返回旧值
19 }
20 }
21 //修改次数增加1
22 modCount++;
23 //将key、value添加至i位置处
24 addEntry(hash, key, value, i);
25 return null;
26 }
重点来了,我们看一下上面代码第6,8行,一个是通过key计算hash值,一个是通过hash值定位到数组中的位置。
1 static int hash(int h) {
2 h ^= (h >>> 20) ^ (h >>> 12);
3 return h ^ (h >>> 7) ^ (h >>> 4);
4 }
5
6 static int indexFor(int h, int length) {
7 return h & (length-1);
8 }
对于HashMap的数组而言,我们需要他里面的数据分布的尽量均匀,最好是每一项都有元素。因为分布的太紧张查询蛮,分布的太分散浪费空间。因为我们在构造函数中capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。所以index的方法就相当于是对lenght取模处理。那么我们为什么要取模处理呢?因为length是2的N次方,当length-1的时候,你会发现得到的结果值 进行地位&运算时候,值与原来的hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
那么所以整体上来讲,HashMap的put方法就是:首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。
这种解决hash冲突的方法叫做【链地址法】,还有其他的比如:再哈希法,开放定址法,建立一个公共溢出区。
具体的实现过程见addEntry方法。
1 void addEntry(int hash, K key, V value, int bucketIndex) {
2 //获取bucketIndex处的Entry
3 Entry<K, V> e = table[bucketIndex];
4 //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
5 table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
6 //若HashMap中元素的个数超过极限了,则容量扩大两倍
7 if (size++ >= threshold)
8 resize(2 * table.length);
9 }
首先,系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。
然后是扩容, 随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。具体扩容的代码很简单,
1 void resize(int newCapacity) { 2 Entry[] oldTable = table; 3 int oldCapacity = oldTable.length; 4 if (oldCapacity == MAXIMUM_CAPACITY) { 5 threshold = Integer.MAX_VALUE; 6 return; 7 } 8 Entry[] newTable = new Entry[newCapacity]; 9 boolean oldAltHashing = useAltHashing; 10 useAltHashing |= sun.misc.VM.isBooted() && 11 (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 12 boolean rehash = oldAltHashing ^ useAltHashing; 13 //transfer函数的调用 14 transfer(newTable, rehash); 15 table = newTable; 16 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 17 } 18 19 void transfer(Entry[] newTable, boolean rehash) { 20 int newCapacity = newTable.length; 21 //这里才是问题出现的关键 22 for (Entry<K,V> e : table) { 23 //遍历旧的Entry数组的每个元素, 24 while(null != e) { 25 //寻找到下一个节点.. 26 Entry<K,V> next = e.next; 27 if (rehash) { 28 e.hash = null == e.key ? 0 : hash(e.key); 29 } 30 //重新计算每个元素在数组中的位置 31 int i = indexFor(e.hash, newCapacity); 32 e.next = newTable[i]; 33 newTable[i] = e; 34 e = next; 35 } 36 }
四,读取数据
通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。
1 public V get(Object key) {
2 // 若为null,调用getForNullKey方法返回相对应的value
3 if (key == null)
4 return getForNullKey();
5 // 根据该 key 的 hashCode 值计算它的 hash 码
6 int hash = hash(key.hashCode());
7 // 取出 table 数组中指定索引处的值
8 for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
9 Object k;
10 //若搜索的key与查找的key相同,则返回相对应的value
11 if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
12 return e.value;
13 }
14 return null;
15 }
2018年5月2日更新:
发现当时jdk1.7的版本和现在的版本中HashMap的实现变化还挺大的。所以这里在重新更新一下,把认识到的变化在这里再做一些补充下,避免后面大家看到了这个对大家产生误导。
第一点:
JDK1.8 之后的 HashMap 底层在解决哈希冲突的时候,就不单单是使用数组加上单链表的组合了,因为当处理如果 hash 值冲突较多的情况下,链表的长度就会越来越长,此时通过单链表来寻找对应 Key 对应的 Value 的时候就会使得时间复杂度达到 O(n),因此在 JDK1.8 之后,在链表新增节点导致链表长度超过 TREEIFY_THRESHOLD = 8 的时候,就会在添加元素的同时将原来的单链表转化为红黑树。我们知道红黑树是一种易于增删改查的二叉树,他对与数据的查询的时间复杂度是 O(logn) 级别,所以利用红黑树的特点就可以更高效的对 HashMap 中的元素进行操作。
第二点:在hash方面,首先,在高位扰动方面,只是简单的h = h ^ (h >>> 16),没有再做那么多的扰动,就得到了hash值。其次,去掉了indexFor这个专门定位的函数,而是在put,get等操作中直接定位,可以看到这些函数中都有这两行。
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
第三点:就是扩容机制,jdk1.7扩容是直接采用头插将老的数据遍历插入到新的table中。在jdk1.8新版本中,我们来看一下扩容机制。
1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table;//首次初始化后table为Null 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold;//默认构造器的情况下为0 5 int newCap, newThr = 0; 6 if (oldCap > 0) {//table扩容过 7 //当前table容量大于最大值得时候返回当前table 8 if (oldCap >= MAXIMUM_CAPACITY) { 9 threshold = Integer.MAX_VALUE; 10 return oldTab; 11 } 12 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 13 oldCap >= DEFAULT_INITIAL_CAPACITY) 14 //table的容量乘以2,threshold的值也乘以2 15 newThr = oldThr << 1; // double threshold 16 } 17 else if (oldThr > 0) // initial capacity was placed in threshold 18 //使用带有初始容量的构造器时,table容量为初始化得到的threshold 19 newCap = oldThr; 20 else { //默认构造器下进行扩容 21 // zero initial threshold signifies using defaults 22 newCap = DEFAULT_INITIAL_CAPACITY; 23 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 24 } 25 if (newThr == 0) { 26 //使用带有初始容量的构造器在此处进行扩容 27 float ft = (float)newCap * loadFactor; 28 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 29 (int)ft : Integer.MAX_VALUE); 30 } 31 threshold = newThr; 32 @SuppressWarnings({"rawtypes","unchecked"}) 33 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 34 table = newTab; 35 if (oldTab != null) { 36 for (int j = 0; j < oldCap; ++j) { 37 HashMap.Node<K,V> e; 38 if ((e = oldTab[j]) != null) { 39 // help gc 40 oldTab[j] = null; 41 if (e.next == null) 42 // 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1) 43 // 扩容都是按照2的幂次方扩容,因此newCap = 2^n 44 newTab[e.hash & (newCap - 1)] = e; 45 else if (e instanceof HashMap.TreeNode) 46 // 当前index对应的节点为红黑树,这里篇幅比较长且需要了解其数据结构跟算法,因此不进行详解,当树的个数小于等于UNTREEIFY_THRESHOLD则转成链表 47 ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap); 48 else { // preserve order 49 // 把当前index对应的链表分成两个链表,减少扩容的迁移量 50 HashMap.Node<K,V> loHead = null, loTail = null; 51 HashMap.Node<K,V> hiHead = null, hiTail = null; 52 HashMap.Node<K,V> next; 53 do { 54 next = e.next; 55 if ((e.hash & oldCap) == 0) { 56 // 扩容后不需要移动的链表 57 if (loTail == null) 58 loHead = e; 59 else 60 loTail.next = e; 61 loTail = e; 62 } 63 else { 64 // 扩容后需要移动的链表 65 if (hiTail == null) 66 hiHead = e; 67 else 68 hiTail.next = e; 69 hiTail = e; 70 } 71 } while ((e = next) != null); 72 if (loTail != null) { 73 // help gc 74 loTail.next = null; 75 newTab[j] = loHead; 76 } 77 if (hiTail != null) { 78 // help gc 79 hiTail.next = null; 80 // 扩容长度为当前index位置+旧的容量 81 newTab[j + oldCap] = hiHead; 82 } 83 } 84 } 85 } 86 } 87 return newTab; 88 }