-
1.7 底层数据结构
- 数组 + 链表、扩容时头插法
-
1.8 底层数据结构
- 数组 + 链表 + 红黑树、扩容时采用 尾插法
- 当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率
-
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?
- 因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题.
- 尾插法还会保持元素原本的顺序
-
为什么HashMap的容量总是2的n次幂?
- 关键代码p = tab[i = (n - 1) & hash],n是一定是2的幂次方,2的幂次方换算成二进制,高位一定是1,然后再减去1,就变成除了低位第一位为0,其他全部为1,所以当hash的二进制和(n-1)的二进制进行位与运算的时候,hash的二进制任何一位变成0或1,那么最终得到的值是不同的,这样扩大了数组散列性
- 2的幂次方:方便位运算、数据均匀分布
- 如果设置的不是2的幂次方,HashMap会计算出与该数最接近的数字
-
扩容机制
-
static final float DEFAULT_LOAD_FACTOR = 0.75f; -
扩容大小为原数组的2倍
-
为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8?
- 数学 —— 泊松分布
- 桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值
-
1.7是先扩容再进行插入
- 当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用
-
load_factor 负载因子越大
- 优点:空间利用率高
- 缺点:Hash冲突概率加大、链表变长、查找效率变低
-
load_factor 负载因子越小
- 优点:Hash冲突概率小、链表短、查找效率高
- 缺点:空间利用率低、频繁扩容耗费性能
-
-
并发情况下的问题
- 插入的元素可能被覆盖
- put的时候链表可能形成环形数据结构,会形成死循环 主要原因还是线程不安全
-
为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键?
- String、Integer 等是 final 类型 ,不可变,保证了 key 的不可更改性,就保证了 Hash值的不可更改性,不会出现放入时和取出时hash不同的情况
- 内部已经重写了eqauls()、hashcode() ,不容易出现hash值的计算错误
-
为什么不直接采用经过
hashCode()处理的哈希码 作为 存储数组table的下标位置?- 容易出现哈希码与数组大小范围不匹配的情况,即计算出来的哈希码可能不在数组大小范围内,从而导致无法匹配存储位置
- HashMap的解决方案:哈希码 & (数组长度 - 1)
-
为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
- 要想让算出来的hash值在数组范围内,就只能取余,即:h % length;但是取余效率低,这个是操作系统决定的,所以采用位运算 &
-
hash计算规则
-
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
put 源码
-
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) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } -
get 源码
-
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }