顾名思义,HashMap
就是以 hash
值为 key
的 Map
集合
HashMap
是以 数组 作为容器的
1 transient Node<K,V>[] table;
Node
是 HashMap
内部实现了 Map.Entry
的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
就是以 hash 值为 key
put
方法:
1 2 3 4 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)
而 putVal
第一个参数就是 hash 值,再看看 hash
方法:
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
可以看到,如果 key 为 null,hash 值为 0;否则:
hash 值为 hashCode()
做一次 16 位右位移异或
为什么这样做,是因为 扰动函数 ,具体请看:知乎传送门
下面提到的 putVal 也有讲
必识的变量 biss:最讨厌看这些静态常量了,每个字母都是大写
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; static final int MAXIMUM_CAPACITY = 1 << 30 ;static final float DEFAULT_LOAD_FACTOR = 0.75f ;static final int TREEIFY_THRESHOLD = 8 ;static final int UNTREEIFY_THRESHOLD = 6 ;static final int MIN_TREEIFY_CAPACITY = 64 ;transient Node<K,V>[] table;transient Set<Map.Entry<K,V>> entrySet;transient int size;transient int modCount;int threshold;final float loadFactor;
resize 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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 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 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0 ) { float ft = (float )newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ? (int )ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null ); if (loTail != null ) { loTail.next = null ; newTab[j] = loHead; } if (hiTail != null ) { hiTail.next = null ; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
扩容后链表元素的位置 直接判断 (e.hash & oldCap)
,结果为 0 就代表与之前的位置相比 没有变化,否则就是有变化
为什么要这样来决定?
我们知道( 如果不知道,看下面的 putVal ),(n - 1) & hash
是元素在数组的位置( n 是容量大小 )
假设 oldCap
为 16,也就是 2^4
n - 1 = 15,即 0000 0000 …… 1111
而 & hash 就是取 hash 的低 4 位,设为 xxxx
此时数组扩容是2倍,也就是 2 * oldCap = 32 = 2^5,& hash 后就是取 hash 的低 5 位
对于低 5 位,对于同一个元素,无非就是
0xxxx 和 没扩容前的低 4 位一样
1xxxx = 原来的低 4 位 + oldCap
但是怎么才能知道是 0 还是 1?
直接 (e.hash & oldCap)
即 hash & 0000 0000 …… 1 0000 不就行了!!
如果 (e.hash & oldCap) = 0
那就是说元素在新数组的位置还是之前的,不变;
如果 (e.hash & oldCap) = 1
那就是说元素在新数组的位置 = 原来的位置 + oldCap
参考文章
putVal 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 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 ; }
尾插法 1.8 之前插入元素是使用头插法,可能觉得新来的被访问的可能性高
但是 1.8 之后却使用尾插法
简单来说:头插法,多线程 插入时,会在扩容的时候造成 Infinite Loop
,即 A 指向 B,B 指向 A
具体是为什么请看:参考文章
总结 首先 HashMap 是实现了 Map 接口的,所以是它的每个元素都是以键值对的形式存放
而这个键值对 是实现了 Map 接口定义的 Node 类,它的成员变量有 hash、key、value,还有下一个节点 next
而存放这些键值对元素的底层就是 Node 数组
当第一次发生插入时,首先是初始化,即使用 resize 方法
插入 1、下标 而插入的时候,数组下标是 key 的 hash 值 & ( 数组长度 - 1 )
这个 hash 值是经过扰动得来的,先通过 hashcode 拿到最初的 hash 值,也就是 32 位的内存地址
然后高 16 位右移和原来的 hash 异或^( 两个不同就为真 )
这时候低 16 位就保留了高低位的信息
然后再和( 数组大小 - 1 )与( & ),拿到低位作为数组下标
而数组大小建议是 2 的幂次方,这样减 1 后低位就是 000…1111,和已经扰动的 hash 值与( & )一下就拿到低位
这样这个低位就有很大的随机性,这个低位来充当数组下标,能更有效的避免哈希碰撞
2、插入 如果这个下标没有元素,就直接赋值
如果有元素
插入结束后,会自增 map 的 size,如果实际大小超过临界值( threshold ),就会进行扩容( threshold = 负载因子 * 容量 ),执行 resize 方法
3、尾插法 1.8 之前插入元素是使用头插法,可能觉得新来的被访问的可能性高。
但是1.8 及以后却使用尾插法。
简单来说:头插法,多线程 插入时,会在扩容的时候造成 Infinite Loop( 无限循环、死循环 )
,形成 环形链表 ,即 A 指向 B,B 指向 A
扩容或初始化 1、初始化 如果是第一次插入,那么需要进行初始化,而初始化是在 resize 方法里面
在 resize 方法里面,因为 HashMap new 的时候只是初始化了负载因子,是 0.75f,而 resize 方法会对容量和临界值( threshold )赋值
如果在构造函数里已经有具体的容量参数,就直接用
如果没有,就用默认的
2、扩容 如果已经初始化
如果目前现在的的容量大于最大容量的上限( MAXIMUM_CAPACITY ),则不会进行扩容,并且设置临界值( threshold )为最大( Integer.MAX_VALUE )
如果现在的容量没到达上限,就会对容量进行 两倍扩容 ( << 1 )
而且再扩大两倍还没超过上限,并且原来的容量大于默认的容量( DEFAULT_INITIAL_CAPACITY 16 )
就会对临界值( threshold )也进行 两倍扩容 ( << 1 )
完成后,再进行元素的重新放置
它会开一个新容量的数组,按照老数组的下标来遍历,把元素都拷进去
如果元素 next 为 null,表示此元素是单个的,直接根据 hash & ( 容量 - 1 ) 得到新的位置进行赋值
如果元素 next 不为 null,再判断是不是红黑树节点
如果是,就按红黑树转
如果不是,那就是链表了
新的链表头的下标不是根据 hash & ( 容量 - 1 ) 得到新的位置
而是根据判断( e.hash & oldCap )是不是等于 0,来决定是 采用( 原始位置 + 原始容量 )得到 新的位置,还是位置不变
它会维护两个链表,一个存着位置不变的元素链,一个存着位置为( 原始位置 + 原始容量 )的元素链
至于为什么根据( e.hash & oldCap )来判断
其他常用的易忘的 1 2 3 4 public V getOrDefault (Object key, V defaultValue) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? defaultValue : e.value; }