HashMap存储结构采用数组+链表(java8以后当链表长度超过8后采用红黑树来存储) ,当存储数据时会先计算key的哈希值,然后使用哈希值计算数组指针,如果数组指针位置已经存在数据,且key不相同,就会采用链表形式存储数据。存储结构如下图:
在jdk1.8中HashMap引入了红黑树,当链表中的元素达到阈值后,并且数组长度大于64,就会将链表转换为红黑树。引入红黑树的主要目的是解决哈希冲突导致的链化严重的问题。
哈希值与数组指针 哈希值计算方法:获取key的hashCode,然后hashCode右移16位,最后将两者进行异或运算。源码如下:
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
为什么没有直接使用key.hashCode()的值呢?这是因为数组长度都是2的幕,比如默认数组长度16=$2^4$,即1<<4
,长度掩码就是15=16-1
,二进制掩码就是1111
。hash与掩码进行按位与运算后得到数组指针,相当于只取了hash的低4位,高位没有用到,增加了指针冲突的概率。因此将hashCode进行右移16位,再进行异或运算,这样做就是在运算中同时使用了高位和低位。
HashMap是通过数组来存储元素。当通过key来后去元素值的时候,首先计算出key的哈希值,然后通过哈希值来计算得到数组坐标,看下put操作中如何计算数组坐标:
1 2 3 4 5 6 7 8 9 final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); ...... }
从代码中可以看出,计算数组坐标的算法是:(n - 1) & hash
,其中n
表示数组长度。所以坐标指针是hash和长度掩码进行与运算。
下图以key="Hello"
为例来说明计算过程:
put插入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
扩容 HashMap采用数组和链表的形式存储数据,即使不扩容理论上可以存储无限多的数据元素。那么为什么要进行扩容呢?HashMap的扩容实际上指得是数组的扩容,默认的数组长度是16,如果不进行扩容,后续数据都依赖链表存储,那么随着数据的增多,查找效率会越来越低,即复杂度是O(n),而不是O(1)。扩容的依据是总数据量,包括所有数组、链表、红黑树中的元素。
1 2 3 4 5 6 7 8 9 10 11 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; static final float DEFAULT_LOAD_FACTOR = 0.75f ;int threshold;final float loadFactor;transient Node<K,V>[] table; public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }
HashMap的无参构造函数中将负载因子loadFactor
变量设置为0.75。但是并没有设置扩容阈值或者容量,table数组初始值也是null。既然默认都是空的,那么看一下put方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { .... } ...... }
看样子会在resize方法中设置容量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { ...... } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } ...... threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; if (oldTab != null ) { ...... } return newTab; }
自定义初始容量 通过下面的构造函数用户可以自定义初始容量:
1 2 3 public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); }
但是,HashMap不会完全按照用户指定容量来扩容,因为HashMap的容量必须是2的幕。所以,会通过tableSizeFor
方法进行换算:
1 2 3 4 5 6 7 8 9 static final int tableSizeFor (int cap) { int n = cap - 1 ; n |= n >>> 1 ; n |= n >>> 2 ; n |= n >>> 4 ; n |= n >>> 8 ; n |= n >>> 16 ; return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; }
比如用户传入的是18,计算过程如下:
运算公式
二进制运算
$n=18-1=17$
$n=17|(17>>>1)=25$
$n=0b10001|0b01000=0b11001$
$n=25|(25>>>2)=31$
$n=0b11001|0b00110=0b11111$
$n=31|(31>>>3)=31$
$n=0b11111|0b00011=0b11111$
$n=31|(31>>>4)=31$
$n=0b11111|0b00001=0b11111$
$n=31|(31>>>5)=31$
$n=0b11111|0b00000=0b11111$
$n=31|(31>>>6)=31$
$n=0b11111|0b00000=0b11111$
return $31+1$; //$32=2^5$
相当于把18二进制的bit位全部赋值位1,然后再加一。$0b10010$ ⇒ $0b11111$ ⇒ $0b100000$。
参考文章 盘点 HashMap 源码中的那些优雅的设计! HashMap底层实现原理详解