一、HashMap底层实现原理(JDK1.8)
先来说说HashMap的几个特点,
1:是一个存储key-value数据对的集合。
2:key和value都可以存储null值。
3:key值不可以重复。
4:无序。
5:线程不安全。
二、HashMap底层数据结构,
HashMap底层是数组+链表+红黑树的实现,HashMap初始化时,是一个集合+链表的结合体,在数据结构中称为"散列链表",即:在集合中,每一个元素存储的是一个链表;JDK1.8之后,HashMap添加了一个属性TREEIFY_THRESHOLD,默认为值为8,当HashMap中put一个元素时,如果数组中该链表的长度达到8,则将链表转换为红黑树。
三、HashMap的属性解析
1 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ 2 //table数组默认长度大小为16,必须为2的N次幂,减少hash碰撞 3 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 4 //table数组的最大长度,2^30 5 static final int MAXIMUM_CAPACITY = 1 << 30; 6 //table扩容因子,table中的元素个数 > table.length * DEFAULT_LOAD_FACTOR时,table数组自动扩容为table.length的两倍 7 static final float DEFAULT_LOAD_FACTOR = 0.75f; 8 //由链表结构转换为红黑树的阈值,当链表中的数据大于8时,将链表结构转换为红黑树存储 9 static final int TREEIFY_THRESHOLD = 8; 10 //由红黑树转换为链表结构的阈值,当红黑树的数量小于6时,将红黑树转换为链表结构存储 11 static final int UNTREEIFY_THRESHOLD = 6; 12 //由链表结构转换为红黑树时,还会有一次判断,只有键值对数量大于MIN_TREEIFY_CAPACITY时才会发生转换。 13 //这是为了避免哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的变化 14 static final int MIN_TREEIFY_CAPACITY = 64; 15 16 //存放元素的数组 17 transient Node<K,V>[] table; 18 transient Set<Map.Entry<K,V>> entrySet; 19 //HashMap键值对的个数 20 transient int size; 21 //修改次数 fail-fast机制;如果遍历时,有其它修改了HashMap,那么将抛出ConcurrentModificationException 22 transient int modCount; 23 //扩容临界值 threshold = size * loadFactor ; 当HashMap中的数组元素个数 > threshold时,触发扩容操作 24 int threshold; 25 //装在因子,默认为0.75f 即:DEFAULT_LOAD_FACTOR 26 final float loadFactor; 27 }
四、HashMap的put(k , v)操作流程
1 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ 2 public V put(K key, V value) { 3 return putVal(hash(key), key, value, false, true); 4 } 5 //1:根据key值,计算hash值,如下: 6 //计算HashMap中数组角标,高位16位 + 低位16位进行^操作,可以将key计算后的hash值更分散,减少hash冲撞 7 static final int hash(Object key) { 8 int h; 9 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 10 } 11 //2:将key-value键值对放入指定的角标位置 12 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 13 boolean evict) { 14 Node<K,V>[] tab; Node<K,V> p; int n, i; 15 //1:如果tab为null,或者talbe的长度=0;初始化table 16 if ((tab = table) == null || (n = tab.length) == 0){ 17 n = (tab = resize()).length; 18 } 19 //(n-1) & hash 找到key在数组中存储的角标位置;如果为空,则直接新建链表节点Node 20 if ((p = tab[i = (n - 1) & hash]) == null){ 21 tab[i] = newNode(hash, key, value, null); 22 } 23 //不为空 24 else { 25 Node<K,V> e; K k; 26 //头结点hash值与新put的key的hash值相同,且头结点的key值与新put的key值相同 27 if (p.hash == hash && 28 ((k = p.key) == key || (key != null && key.equals(k)))){ 29 e = p; 30 } 31 //如果头结点为红黑树节点TreeNode 32 else if (p instanceof TreeNode){ 33 // 红黑树的put方法比较复杂,putVal之后还要遍历整个树,必要的时候修改值来保证红黑树的特点 34 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 35 } 36 //头结点为链表节点Node 37 else { 38 //遍历链表 39 for (int binCount = 0; ; ++binCount) { 40 //如果遍历到链表尾部,还没发现key值冲突的节点 41 if ((e = p.next) == null) { 42 //新建节点,并加到链表底部 43 p.next = newNode(hash, key, value, null); 44 //如果新增节点后,节点的数量达到阈值,则将链表转换为红黑树 45 if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st 46 treeifyBin(tab, hash);//链表转换红黑树 47 } 48 break; 49 } 50 //如果key值一致,并且hash一致,替换 51 if (e.hash == hash && 52 ((k = e.key) == key || (key != null && key.equals(k)))) 53 break; 54 p = e; 55 } 56 } 57 //将节点值更新为传递过来的value值,并返回key对应的旧值 58 if (e != null) { // existing mapping for key 59 V oldValue = e.value; 60 if (!onlyIfAbsent || oldValue == null) 61 e.value = value; 62 afterNodeAccess(e); 63 return oldValue; 64 } 65 } 66 ++modCount; 67 //如果达到扩容的阈值,则进行扩容 68 if (++size > threshold){ 69 resize(); 70 } 71 afterNodeInsertion(evict); 72 return null; 73 } 74 }
五、HashMap的get(k)操作流程
1 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ 2 public V get(Object key) { 3 Node<K,V> e; 4 //如果找不到对应的Node就返回null,如果找到就返回Node.value 5 return (e = getNode(hash(key), key)) == null ? null : e.value; 6 } 7 //获取Node操作 8 final Node<K,V> getNode(int hash, Object key) { 9 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 10 //tab为不为空,并且长度大于0,并且角标[(tab.length-1) & hash]位置不为空 11 if ((tab = table) != null && (n = tab.length) > 0 && 12 (first = tab[(n - 1) & hash]) != null) { 13 //第一个Node就是要找的节点,直接返回 14 if (first.hash == hash && // always check first node 15 ((k = first.key) == key || (key != null && key.equals(k)))){ 16 return first; 17 } 18 if ((e = first.next) != null) { 19 //如果是树,遍历红黑树复杂度是O(log(n)),得到节点值 20 if (first instanceof TreeNode){ 21 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 22 } 23 //如果是链表,遍历链表负责度是O(n),得到节点值 24 do { 25 if (e.hash == hash && 26 ((k = e.key) == key || (key != null && key.equals(k)))) 27 return e; 28 } while ((e = e.next) != null); 29 } 30 } 31 return null; 32 } 33 }
六、总结
到这里,HashMap的原理已经基本说明白了,下面说一下我认为的比较重要的点:
1:HashMap的数据结构是数组+链表+红黑树,每次获取结果时,还需要遍历链表或者红黑树中的元素,因此,我们希望这个HashMap里面的元素位置分布均匀一些,如果每个数组每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,就可以马上知道赌赢位置的元素就是我们要的,而不需要再去遍历链表或者红黑树;此时,效率才是最高的。
那么,问题来了,获取每个key的hash值之后,如果才能将结果尽量的均匀分布在数组的索引上呢?
大家首先想到的应该是把hash值对数组长度做取模运算,这样一来,元素的分布是比较均匀的,但是,Java的取模运算的性能消耗还是比较大的,能不能找一种更快速,性能更高的方式呢?
HashMap中是这样做的:h & (table.length - 1);首先使用hash算法算得key的值(HashMap在这里将高16位和低16位进行了运算,是的分布更均匀),然后跟数组的长度-1做一次按位"&"运算(两个位的值同时为1,做"&"运算的结果采为1,其它结果都为0)。看上去很简单,其实不然,这里就需要说说为什么数组的长度必须为2^n(2的n次方)时,HashMap的分布才更均匀。
第一个数组长度为16,16-1=15 ,15二进制表示为1111;
第二个数组长度为15,15-1=14,14二进制表示为1110;
两组key的值分别为8和9,二进制标示分别为1000和1001;
1111 & 1000 = 1000 //数组长度16 15 & 8
1111 & 1001 = 1001 //数组长度16 15 & 9
1110 & 1000 = 1000 //数组长度16 14 & 8
1110 & 1001 = 1111 //数组长度16 14 & 9
我们发现,当数组长度为15时,hashcode的值会与14(1110)进行按位"&"操作,那么最后一位是0,而0001、0011、0101、1001、1011、0111、1101、1111这几个位置永远都不能存放数据了,空间浪费相当大,更糟的这几种情况是,数组中可以使用的位置比数组长度晓蕾很多,这意味着进一步增加了hash碰撞的几率,减慢了查询的效率。
所以,当数组长度为2^n次幂时,不同的key算的的index相同的几率较小,那么数据在数组中分布就比较均匀,也就是说碰撞的几率较小,相对的查询的时候遍历的次数就少,这样查询效率也就高了。
2:HashMap的resize()方法
当HashMap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就需要对HashMap的数组进行扩容,数组扩容的这个操作也会出现在ArrayList中,所以这是一个通用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap在什么时候进行扩容呢?当HashMap中的元素个数超过length*loadFactor时,就会进行扩容。loadFactor的值默认为0.75,也就是说默认情况下数组大小为16,那么当HashMap数组中的元素个数超过16*0.75=12的时候,就需要将数组的大小扩展为2*16=32,然后计算每个元素在数组中的位置,而这时一个非常消耗性能的操作。
所以,如果我们已经预知HashMap中元素的个数,那么预设HashMap元素的个数能够有效提高HashMap的性能。例如:如果元素预计为1000,我们必须new HashMap(2048)才最合适。