HashMap(JDK1.8)

1.整体结构

HashMap是开发中经常使用的集合,典型的key-value数据结构。

它的整体结构(jdk1.8版本)是有数组+链表+红黑树组成。

众所周知,数组的特性是寻址容易,插入和删除困难,链表的特性是寻址困难,插入和删除容易。

数组在指定下标下,寻址时间复杂度是O(1),而链表的插入和删除时间复杂度是O(1)。

HashMap结构

 

1.1基本原理

HashMap使用了桶的概念,一个hash一个桶,将node扔进桶里面,桶内可以是一个节点,可以是链表,也可以是红黑树,桶里面的节点有相同的key则替换value,如果桶的3/4数量都存了value,则进行扩容。

类注释

1.key和value允许存null值。

2.影响HashMap性能的两个主要参数,capacity和loadfactor,capacity是桶的容量,loadfactor是负载因子。

3.线程不安全,如果需要线程安全,使用Collections.synchronizedMap。

4.迭代的过程中,结构发生变化,会快速失败。

5.哈希值得分布概率服从泊松分布,哈希冲突严重的情况下,链表会转换成红黑树,但发生的概率是少于1千万分之一,


 
 
  1. 0: 0 .60653066
  2. 1: 0 .30326533
  3. 2: 0 .07581633
  4. 3: 0 .01263606
  5. 4: 0 .00157952
  6. 5: 0 .00015795
  7. 6: 0 .00001316
  8. 7: 0 .00000094
  9. 8: 0 .00000006

 

1.2常见属性


 
 
  1. //初始容量16。
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  3. //HashMap最大容量。
  4. static final int MAXIMUM_CAPACITY = 1 << 30;
  5. //负载因子,默认0.75(3/4)。
  6. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  7. //链表阈值,链表等于8,即转换成红黑树。
  8. static final int TREEIFY_THRESHOLD = 8;
  9. //红黑树阈值,红黑树长度缩小为6时,转换为链表。
  10. static final int UNTREEIFY_THRESHOLD = 6;
  11. //链表转换红黑树,数组容量必须大于等于64
  12. static final int MIN_TREEIFY_CAPACITY = 64;
  13. //链表节点。
  14. static class Node<K,V> implements Map.Entry<K,V>
  15. //红黑树节点。
  16. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
  17. //记录HashMap结构变化的次数。
  18. transient int modCount;
  19. //HashMap实际大小。
  20. transient int size;
  21. //存放数据的数组。
  22. transient Node<K,V>[] table;

2.新增和查询

2.1新增

put 流程图

 

HashMap新增的步骤

  1. 转换hash并取模定位数组下标获取元素。
  2. 是空值,跳转第⑥。
  3. 不是空值,是单节点或链表,存值到队尾,跳转第⑤。
  4. 不是空值,是红黑树,跳转第⑥。
  5. 是否链表长度等于8且数组容量大于等于64,是则转换红黑树,否则结束。
  6. 存值,结束。

 
 
  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2. boolean evict) {
  3. Node<K,V>[] tab; Node<K,V> p; int n, i;
  4. //如果table为空,进行初始化。
  5. if ((tab = table) == null || (n = tab.length) == 0)
  6. n = (tab = resize()).length;
  7. //拿到对应位置的元素,为空,则赋值
  8. if ((p = tab[i = (n - 1) & hash]) == null)
  9. tab[i] = newNode(hash, key, value, null);
  10. else {
  11. Node<K,V> e; K k;
  12. //hash和key相同,赋值到e。
  13. if (p.hash == hash &&
  14. ((k = p.key) == key || (key != null && key.equals(k))))
  15. e = p;
  16. //如果p为红黑树,添加到红黑树中。
  17. else if (p instanceof TreeNode)
  18. e = ((TreeNode<K,V>)p).putTreeVal( this, tab, hash, key, value);
  19. else {
  20. //hash冲突,进行自旋
  21. for ( int binCount = 0; ; ++binCount) {
  22. if ((e = p.next) == null) {
  23. //保存到队尾。
  24. p.next = newNode(hash, key, value, null);
  25. //链表长度大于等于8,尝试转换成红黑树。
  26. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  27. treeifyBin(tab, hash);
  28. break;
  29. }
  30. if (e.hash == hash &&
  31. ((k = e.key) == key || (key != null && key.equals(k))))
  32. break;
  33. p = e;
  34. }
  35. }
  36. if (e != null) { // existing mapping for key
  37. V oldValue = e.value;
  38. //是否覆盖值
  39. if (!onlyIfAbsent || oldValue == null)
  40. e.value = value;
  41. //钩子方法,交给子类实现。
  42. afterNodeAccess(e);
  43. return oldValue;
  44. }
  45. }
  46. //记录结构改变次数
  47. ++modCount;
  48. //超过capicity阈值,扩容。
  49. if (++size > threshold)
  50. resize();
  51. //钩子方法,交给子类实现。
  52. afterNodeInsertion(evict);
  53. return null;
  54. }

2.2查询


 
 
  1. public V get(Object key) {
  2. Node<K,V> e;
  3. //key转换hash,传参到getNode
  4. return (e = getNode(hash(key), key)) == null ? null : e.value;
  5. }
  6. getNode关键代码:
  7. if ((e = first.next) != null) {
  8. if (first instanceof TreeNode)
  9. //如果是红黑树,增加到红黑树上。
  10. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  11. do {
  12. //自旋,找到key并返回
  13. if (e.hash == hash &&
  14. ((k = e.key) == key || (key != null && key.equals(k))))
  15. return e;
  16. } while ((e = e.next) != null);
  17. }

小结:新增逻辑通过hash算法+自旋大大的降低了时间复杂度,保证value能迅速进入队列,而且还留下一个钩子方法,给予子类扩展,钩子方法使用可以参考LinkedHashMap。

 

3.哈希算法

hash中文名叫哈希或散列,hash没有固定的公式。

百科描述哈希(散列)算法,只要符合离散思想的算法都能成为哈希(散列)算法,离散的概率都符合泊松分布。

HashMap每次存值、取值、删除操作都必须将key换算成hash值,平时说的hash冲突,是指hash值相同。

Hash(Object key)是HashMap转换hash值得方法,将key转换成hash,让高16位参与^运算,降低hash冲突的概率。


 
 
  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }
  5. put方法代码中,得到hash值后再进行取模运算,如下代码:
  6. if ((p = tab[i = (n - 1) & hash]) == null)

 

通过多次debug,发现i整好是hash除length的余数,所以hash%length=(n-1)&hash。

注意:位运算不需要区分左和右

Hash % length = (n-1) & hash 如果看不懂,换成a%b=a&(b-1),这样会清晰很多。

例如 9%8 =1 转换成9 & (8-1) = ?

9   二进制:1001

8-1=7 二进制:0111

&运算  : 0001 = 1

可得: 9% 8 = 9&(8-1)

 

但 9% 7 =2 不等于 9 & (7-1) ,为什么呢?

9 二进制: 1001

7-1 二进制: 0100

&位运算: 0000 = 0

这不是推翻了a%b=a&(b-1),先别急。

我们重新查看HashMap的table和resize()的注释。


 
 
  1. //查看这两行代码的注释,这里不展示注释
  2. transient Node<K,V>[] table;
  3. final Node<K,V>[] resize() {}

它们俩的注释都提到了数组长度为2次幂,构造器初始化HashMap时,会通过tableSizefor来转换成传入length最邻近的2次幂值。

所以a%b=a&(b-1)公式的前置条件是b=2^n,

最终公式:a % 2^n = a & (2^n -1 )。

 

为什么取得数组下标的算法是(n-1) & hash呢,而不是用%?

&位运算直接对内存进行二进制运算,%取模采取十进制运算;

众所周知,操作系统对二进制直接进行运算,而十进制需要转换成二进制,这个过程让%比&慢。

 

4.负载因子为什么是0.75f

初始化时没有赋予负载因子的值,默认是0.75,为什么是0.75,不是0.5,也不是0.8或1。

假设负载因子是1,长度为8,HashMap的扩容阈值是1*8=8,当put第9个值时,HashMap才进行扩容,但是数组内部可能产生很严重的hash冲突。

仅供参考(不完全符合泊松分布)

JDK的官方原文描述:

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

大致意思是0.75是时间与空间成本之间一个很好的权衡,太低会经常扩容,太高增加查找的成本,可以大大降低hash冲突的概率。

那么为什么是0.75,不是0.7或者0.8?

官方没有说明,个人猜测,因为hashmap的长度是2次幂,如果是0.7(7/10)或0.8(4/5),分数无法被2次幂整除,0.5(1/2)太低,造成频繁扩容浪费资源,所以0.75是负载因子理想的值。

 

5.总结

回顾一下,HashMap是k-v的典型数据结构,结合了数组和链表的优点,线程不安全,新增和查找借助hash算法和取模算法快速定位数组下标,大大降低了时间复杂度。api本质还是对数组和链表的操作进行封装,大致可归纳为是转换hash值和自旋以及边界值判断。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值