HashMap JDK 1.8 以前 JDK1.8 之前HashMap底层通过 数组和链表 结合。HashMap通过Key的hashcode
经过扰动函数处理得到hash值,然后通过(n - 1) & hash
判断当前元素的存放位置(n指数组长度),如果当前位置存在元素,就判断该元素与要存入的元素的hash值与key是否相同,如果相同直接覆盖,如果不同就通过拉链法解决冲突。
扰动函数就是HashMap的hash
方法,使用hash
方法可以减少哈希冲突。 两个版本的hash
方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static int hash (int h) { h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); } static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
jdk1.7的hash
方法扰动了四次,所以性能不如jdk 1.8
拉链法 即创建一个链表数组,这就是1.7数组+链表的由来,数组中的每一格就是一个链表,如果遇到哈希冲突,则将冲突的值加入链表即可。
jdk 1.8 以后 jdk 1.8 以后,HashMap的底层变为数组+链表+红黑树。
为什么要改成“数组+链表+红黑树”? 主要是为了提升在哈希冲突严重时(链表过长)的查找性能,使用链表的查找性能是O(n),而使用红黑树是O(logn)。
重要初始属性 1 2 3 4 5 6 7 8 9 10 11 12 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 ;
扩容原则 对于插入操作,默认情况下使用链表节点,当同一个索引位置的节点在**新增达到9个(阈值默认为8)**:
如果此时数组长度大于等于64,则触发链表节点转为红黑树节点(treeifyBin)
如果数组长度小于64,则先对数组扩容
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 void treeifyBin (Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1 ) & hash]) != null ) { TreeNode<K,V> hd = null , tl = null ; do { TreeNode<K,V> p = replacementTreeNode(e, null ); if (tl == null ) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null ); if ((tab[index] = hd) != null ) hd.treeify(tab); } }
对于移除操作,当用一个索引位置的节点在移除后达到6个 ,并且该索引位置的节点是红黑树节点,则触发红黑树节点转为链表节点(untreeify)
为什么链表转红黑树的阈值是8? 简单来说,阈值为8是在时间和空间上权衡的结果。 红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,没必要付出2倍的空间。 理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,当链表中节点个数为8时的概率为 0.00000006,这个概率足够低,并且到达8个节点时,红黑树的性能优势也会展现出来,因此8是一个合理的数字。
为什么转回链表节点是用的6而不是复用8? 如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。
默认初始容量 HashMap的默认初始容量为16,且HashMap的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。
1 2 3 4 5 6 7 8 9 10 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 ; }
HashMap 的长度为什么是 2 的幂次方? 计算索引位置的公式为:(n - 1) & hash
,当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值,此时任何值跟 n - 1 进行 & 运算的结果为该值的低 N 位,达到了和取模同样的效果,实现了均匀分布。
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 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 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 { 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 ; }
总结:
判断table
数组是否为空,若为空就进行resize()
扩容,不是就通过(n - 1) & hash
得到存储键值对的索引
判断索引处是否为空,为空就直接插入到对应数组
若不为空, 首先判断key是否已经存在,若存在就直接覆盖
否则,判断插入的是不是红黑树节点,如果是就放入树中
如果不是就遍历链表插入链表尾部,然后判断是否需要转为红黑树
若节点数量达到阈值,执行treeifyBin
方法,如果数组长度大于等于64,才会转为红黑树,否则对数组扩容
判断数组的size
是否大于阈值,若大于就执行resize()
扩容
再来对比一下jdk 1.7中的put方法 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public V put (K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null ) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; }
大致分为两个步骤:
如果当前数组位置没有元素就直接插入
否则,遍历以这个元素为头节点的链表,依次和插入的key比较,如果相同则直接覆盖,不同就采用头插法插入元素。
ConcurrentHashMap 和HashMap的区别