HashMap简介
HashMap是一个由数组+链表/红黑树组成的数据结构,主要是以key->value的形式对数据进行存储。在JDK 1.7及以前的版本中,是由数组+链表的形式组成,在JDK 1.8及之后的版本中当链表长度达到8时就会将链表转换为红黑树
JDK 1.7
JDK 1.8
基本成员变量分析
节点元素
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}}
节点内部类,hashmap中存储的key->value对都会转化成Node
数组
transient Node<K,V>[] table;
初始化数组长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
负载因子
final float loadFactor;
默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子大小默认为0.75,即当hashmap中的元素达到这个值时需要进行扩容。例如:数组长度为16的hashmap,当其中的元素个数达到16 * 0.75 = 12时(当要添加第13个元素时)需要对hashmap进行扩容
hashmap元素个数
transient int size;
表示hashmap中的元素个数,当我们往hashmap里添加一个元素的时候就会对size进行+1
扩容阈值
int threshold;
threshold = table.length loadFactor。当hashmap中的元素个数超过这个值时就需要对hashmap进行扩容。例如:当前table大小为16,负载因子为0.75,即当前threshold为16 0.75 = 12,其中元素个数为12,当我们需要再往hashmap中添加元素时就需要对其进行扩容
hash运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
当我们往hashmap中添加元素时需要对元素的key进行hash算法生成一个32位的hashcode,即(hashcode)异或(hashcode右移16位)
hashcode右移16位就是将低16位的值舍弃,然后将高16位的值移到低16位,高16位用0补齐。例如:一个key对应生成的hashcode值是
原hashcode 1111 1111 1111 1111 0000 1111 0000 0101
右移16位后 0000 0000 0000 0000 1111 1111 1111 1111
原hashcode^右移16位后 1111 1111 1111 1111 1111 0000 1111 1010
对原hashcode右移后再进行异或操作主要是让高位也可以参与运算
put/get方法原理
当我们向hashmap中put元素时(无hash冲突时)
- 将key值进行hash运算得到hash值
- 将得到的hash值跟数组长度进行与运算。相当于取模(取模运算速度比与运算慢)。即可以获取到一个0~table.length - 1的值index。例如:table长度为16,则可以获取到一个0~15之间的一个数
- 将步骤3中获取到的index值作为数组下标,将新加入的节点放到table[index]中
无hash冲突时
put方法
get方法
hash冲突
- 2个不同的key但是经过hash运算后得到相同的值(发生概率较低)
- 2个不同的key经过hash运算后得到不同的值,但是该值与table.length - 1进行与运算后得到的下标值相同
put方法
下图中先在map中put了name1 -> 张三,其在下标1的位置上,此时需要put name2 -> 李四,此时name2通过hash运算再跟(table.length - 1)进行与运算后得到的下标也是1,那么,name2 -> 李四这个节点就会作为链表拼接在name1 -> 张三后面
get方法
红黑树解决hash冲突问题
链表是为了解决hash冲突的问题。但是在hash冲突很严重的情况下, 特别在极端情况下可能会退化成链表,而链表的查询时间复杂度是O(N)的,所以为了提升性能能当链表的长度达到8时就会转换成红黑树(时间复杂度O(logN))
既然红黑树的查询性能优于链表为什么在hash冲突时直接使用红黑树呢?
其实主要是因为红黑树需要额外的内存存储左子节点、右子节点及父节点的引用,所以占用的内存较多
为什么在链表长度为8(插入第9个元素时)的时候对其专为红黑树呢?源码中的解释为,当链表长度达到8时的概率非常低,所以选择了8
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are: 0: 0.60653066 1: 0.30326533 2: 0.07581633 3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
扩容机制
hashmap在元素个数达到扩容阈值(threshold)时,会将数组的长度变为原来的2倍。例如:一个初始化长度为16的hashmap,当节点的元素个数达到12的时候就会申请一个新的数组,然后将原来的hashmap中的元素重新映射到新的数组对应的位置上,并且链表中的元素会逆序插入新的hashmap中。示意图如下

扩容机制示意图
循环链表问题
在JDK 1.7中hashmap在并发情况下会产生循环链表,导致cpu 100%。并发情况下可以使用ConcurrentHashMap。其扩容方法如下
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
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);
}
}
}
hashmap扩容源码
还是以扩容机制是以图中扩容前的数据为例,此时map中有元素:(1, A) -> (3, C),(2, B)
现在我们假设有2个线程并发执行,而且此时需要对map进行库容
| 序号 | 线程1 | 线程2 |
|---|---|---|
| 1 | 线程1执行到上述源码第12行,Entry |
|
| 2 | 此时线程2对该map进行扩容,并且完成后让出cpu资源。此时map中的数据为(假设扩容后数据还在原来的位置) (3, C) -> (1, A), (2, B) |
|
| 3 | 此时由于头结点是(3, C),所以将(1,A)的next元素指向(3,C)。由于(3, C)的next原本指向了(1, A),而此时又把(1, A)的next指向了(3, C)由此就产生了循环链表(3, C)-> (1, A) -> (3, C) |
为什么数组长度一定是2的次方
为了减少hash冲突的概率。因为这样table.length - 1的二进制数一定是高位是0,而地位全是1。例如:十进制数15对应的二进制数是 0000 0000 0000 0000 0000 0000 0000 1111。如果不满足上述条件,例如:十进制数14对应的二进制数是0000 0000 0000 0000 0000 0000 0000 1110,难么当一个hash值与这个数进行与操作时,hash值的最后一位得到的结果一定是0,这样就会破坏hash算法随机分布的特性,从而增加hash冲突的风险。
同时数组长度是2的次方,那么当扩容后元素要么在原来的位置没动,要么在原来的位置 + 原来的数组长度的下标位置下。例如:数组长度为16的map,在扩容为长度为32时,原来在下标1位置下的元素,要么还在下标1的位置,要么在1 + 16 = 17的位置下
**
