1、HashMap-底层数据结构

  1. 1HashMap底层数据结构=数组+链表+红黑树。
  2. 2HashMap字段属性有:
  3. /**
  4. * 序列化和反序列化时,通过该字段进行版本一致性验证
  5. */
  6. private static final long serialVersionUID = 362498820763181265L;
  7. /**
  8. * 默认 HashMap 集合初始容量为16(必须是 2 的倍数)
  9. */
  10. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //左移4位,相当与2^4
  11. /**
  12. * 集合的最大容量,如果通过带参构造指定的最大容量超过此数,默认还是使用此数
  13. */
  14. static final int MAXIMUM_CAPACITY = 1 << 30;//左移30位,相当于2^30
  15. /**
  16. * 默认的填充因子,与扩容有关
  17. */
  18. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  19. /**
  20. * 当桶(bucket)上的结点数大于这个值时会转成红黑树(JDK1.8新增)
  21. */
  22. static final int TREEIFY_THRESHOLD = 8;
  23. /**
  24. * 当桶(bucket)上的节点数小于这个值时会转成链表(JDK1.8新增)
  25. */
  26. static final int UNTREEIFY_THRESHOLD = 6;
  27. /**
  28. * (JDK1.8新增)
  29. * 当集合中的容量大于这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,
  30. * 而不是树形化 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD=4 * 8
  31. */
  32. static final int MIN_TREEIFY_CAPACITY = 64;
  33. 3、重要属性:
  34. /* ---------------- Fields -------------- */
  35. /**
  36. * 存储(位桶)的数组,在第一次使用时初始化,长度总是2的幂次方
  37. * 每一个Node本质都是一个单向链表
  38. */
  39. transient Node<K,V>[] table;
  40. /**
  41. * 保存缓存的entrySet()
  42. */
  43. transient Set<Map.Entry<K,V>> entrySet;
  44. /**
  45. * HashMap大小,它代表HashMap保存的键值对的多少
  46. */
  47. transient int size;
  48. /**
  49. * 记录集合被修改的次数,主要用于迭代器中的快速失败
  50. */
  51. transient int modCount;
  52. /**
  53. * 下一次HashMap扩容的大小: 调整大小的下一个大小值(容量*加载因子)。
  54. * 计算公式:capacity * load factor,这个值是当前已占用数组长度的最大值。
  55. */
  56. int threshold;
  57. /**
  58. * 存储负载因子的常量,计算HashMap的实时装载因子的方法为:size/capacity--table的长度length
  59. */
  60. final float loadFactor;

1-1、HashMap-底层数据结构-宏观-示意图

容器(Map)-HashMap-源码分析 - 图2

1-2、HashMap-底层数据结构-细分-示意图

容器(Map)-HashMap-源码分析 - 图3

1-3、HashMap-底层数据结构-单向链表实现

  1. 1Node是单向链表,它实现了Map.Entry接口。链表查询时间复杂度:O(N)
  2. 1-1Node是一个静态内部类,一种数组和链表相结合的复合结构。
  3. 2、源代码:
  4. static class Node<K,V> implements Map.Entry<K,V> {
  5. final int hash;
  6. final K key;
  7. V value;
  8. Node<K,V> next;
  9. //构造函数:Hash值,键,值,下一个节点
  10. Node(int hash, K key, V value, Node<K,V> next) {
  11. this.hash = hash;
  12. this.key = key;
  13. this.value = value;
  14. this.next = next;
  15. }
  16. public final K getKey() { return key; }
  17. public final V getValue() { return value; }
  18. public final String toString() { return key + "=" + value; }
  19. public final int hashCode() {
  20. return Objects.hashCode(key) ^ Objects.hashCode(value);
  21. }
  22. public final V setValue(V newValue) {
  23. V oldValue = value;
  24. value = newValue;
  25. return oldValue;
  26. }
  27. //判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true
  28. public final boolean equals(Object o) {
  29. if (o == this)
  30. return true;
  31. if (o instanceof Map.Entry) {
  32. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  33. if (Objects.equals(key, e.getKey()) &&
  34. Objects.equals(value, e.getValue()))
  35. return true;
  36. }
  37. return false;
  38. }
  39. }

1-4、HashMap-底层数据结构-红黑树实现

  1. 1、实现了LinkedHashMap.LinkedHashMapEntry<K,V>,红黑树查询时间复杂度:O(logN)
  2. static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
  3. TreeNode<K,V> parent; // 红黑树的根节点
  4. TreeNode<K,V> left; //左树
  5. TreeNode<K,V> right; //右树
  6. TreeNode<K,V> prev; // 上一个几点
  7. boolean red; //是否是红树
  8. TreeNode(int hash, K key, V val, Node<K,V> next) {
  9. super(hash, key, val, next);
  10. }
  11. /**
  12. * 返回当前节点的根节点
  13. */
  14. final TreeNode<K,V> root() {
  15. for (TreeNode<K,V> r = this, p;;) {
  16. if ((p = r.parent) == null)
  17. return r;
  18. r = p;
  19. }
  20. }
  21. ...
  22. }

1-5、HashMap-底层数据结构-hash值计算

  1. 1Hash的计算实现:
  2. //将传入的参数key本身的hashCode与h无符号右移16位进行二进制异或运算得出一个新的hash值。
  3. //右移16位,是与HashMap的table数组下计算标有关系,当发生较大碰撞时也用树形存储降低了冲突。既减少了系统的开销。
  4. static final int hash(Object key) {
  5. int h;
  6. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  7. }
  8. 2hash函数Demo:
  9. key.hashCode() 1111 1111 1111 1111 1111 0000 1010 1101
  10. key.hashCode() >>>16 0000 0000 0000 0000 1111 1111 1111 1111
  11. -------------------------------------------------------------------- ^
  12. ^异或运算后得: 1111 1111 1111 1111 0000 1111 0101 0010

2、HashMap-构造方法

  1. 1、默认构造函数
  2. public HashMap() {
  3. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  4. }
  5. 2、指定容量大小和默认负载因子大小(0.75f)
  6. public HashMap(int initialCapacity) {
  7. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  8. }
  9. 3、指定容量大小和负载因子大小
  10. public HashMap(int initialCapacity, float loadFactor) {
  11. //指定的容量大小不可以小于0
  12. if (initialCapacity < 0)
  13. throw new IllegalArgumentException("Illegal initial capacity: " +
  14. initialCapacity);
  15. //如果初始化容量大于2的30次方,则初始化容量都为2的30次方
  16. if (initialCapacity > MAXIMUM_CAPACITY)
  17. initialCapacity = MAXIMUM_CAPACITY;
  18. //指定的负载因子不可以小于0或为Null
  19. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  20. throw new IllegalArgumentException("Illegal load factor: " +
  21. loadFactor);
  22. // 设置"加载因子"
  23. this.loadFactor = loadFactor;
  24. // 设置"HashMap阈值",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
  25. this.threshold = tableSizeFor(initialCapacity);
  26. }
  27. /**
  28. * 返回大于等于initialCapacity的最小的二次幂数值。
  29. * >>> 操作符表示无符号右移,高位取0。
  30. * | 按位或运算
  31. */
  32. static final int tableSizeFor(int cap) {
  33. int n = cap - 1;
  34. n |= n >>> 1;
  35. n |= n >>> 2;
  36. n |= n >>> 4;
  37. n |= n >>> 8;
  38. n |= n >>> 16;
  39. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  40. }
  41. 4、传入一个Map集合,将Map集合中元素Map.Entry全部添加进HashMap实例中
  42. public HashMap(Map<? extends K, ? extends V> m) {
  43. this.loadFactor = DEFAULT_LOAD_FACTOR;
  44. putMapEntries(m, false);
  45. }
  46. /**
  47. * Implements Map.putAll and Map constructor
  48. */
  49. final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  50. int s = m.size();
  51. if (s > 0) {
  52. if (table == null) { // pre-size
  53. float ft = ((float)s / loadFactor) + 1.0F;
  54. int t = ((ft < (float)MAXIMUM_CAPACITY) ?
  55. (int)ft : MAXIMUM_CAPACITY);
  56. if (t > threshold)
  57. threshold = tableSizeFor(t);
  58. }
  59. else if (s > threshold)
  60. resize();
  61. for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
  62. K key = e.getKey();
  63. V value = e.getValue();
  64. putVal(hash(key), key, value, false, evict);
  65. }
  66. }
  67. }

3、HashMap-存储机制-添加元素

  1. 1、源码分析:.put()方法
  2. public V put(K key, V value) {
  3. return putVal(hash(key), key, value, false, true);
  4. }
  5. /**
  6. *
  7. * @param hash 索引的位置
  8. * @param key 键
  9. * @param value 值
  10. * @param onlyIfAbsent true 表示不要更改现有值
  11. * @param evict false表示table处于创建模式
  12. * @return
  13. */
  14. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  15. Node<K,V>[] tab;
  16. Node<K,V> p;
  17. int n, i;
  18. //如果table为null或者长度为0,则进行初始化、
  19. //resize()方法本来是用于扩容,由于初始化没有实际分配空间,这里用该方法进行空间分配
  20. if ((tab = table) == null || (n = tab.length) == 0)
  21. n = (tab = resize()).length;
  22. //得到该对象的保存位, hash & (length-1)运算等价于对 length 取模,也就是 hash%length。
  23. //但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)。
  24. if ((p = tab[i = (n - 1) & hash]) == null)
  25. //tab[i] 为null,直接将新的key-value插入到计算的索引i位置
  26. tab[i] = newNode(hash, key, value, null);
  27. else { //tab[i] 不为null,表示该位置已经有值了,需要做哈希冲突判断
  28. Node<K,V> e; K k;
  29. //检查第一个Node,判断p是不是要找的值
  30. if (p.hash == hash &&
  31. ((k = p.key) == key || (key != null && key.equals(k))))
  32. e = p; //节点key已经有值了,直接用新值覆盖
  33. else if (p instanceof TreeNode) //该节点是红黑树结构
  34. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  35. else { //该节点是链表结构
  36. for (int binCount = 0; ; ++binCount) {
  37. if ((e = p.next) == null) {
  38. p.next = newNode(hash, key, value, null);
  39. //链表长度大于8,转换成红黑树
  40. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  41. //treeifyBin方法中判断长度是否大于最小红黑树容量64,小于则继续扩容,大于则转为红黑树
  42. treeifyBin(tab, hash);
  43. break;
  44. }
  45. //key已经存在直接覆盖value
  46. if (e.hash == hash &&
  47. ((k = e.key) == key || (key != null && key.equals(k))))
  48. break;
  49. p = e;
  50. }
  51. }
  52. //此处不为空,表示链表中该位置有相同的key值
  53. if (e != null) { // existing mapping for key
  54. V oldValue = e.value;
  55. if (!onlyIfAbsent || oldValue == null)
  56. e.value = value;
  57. afterNodeAccess(e);
  58. return oldValue;//返回存在的Value值
  59. }
  60. }
  61. ++modCount;
  62. //如果当前大小大于门限,则进行扩容
  63. if (++size > threshold)
  64. resize();
  65. afterNodeInsertion(evict);
  66. return null;
  67. }
  68. //空的方法实现, LinkedHashMap 会用到,LinkedHashMap 是继承的 HashMap,并且重写了该方法。
  69. afterNodeAccess(e);
  70. afterNodeInsertion(evict)
  71. ==>
  72. void afterNodeAccess(Node<K,V> p) { }
  73. void afterNodeInsertion(boolean evict) { }
  74. 2、.put()方法源码执行过程:
  75. 2-1、判断键值对数组 table 是否为空或为null,否则执行resize()进行扩容;
  76. 2-2、根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向2-6,如果table[i]不为空,转向2-3
  77. 2-3、判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向2-4,这里的相同指的是hashCode以及equals
  78. 2-4、判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向2-5
  79. 2-5、遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  80. 2-6、插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。
  81. 2-7、如果新插入的key不存在,则返回null,如果新插入的key存在,则返回原key对应的value值(注意新插入的value会覆盖原value值)
  82. 3、.put()方法分析-问题思考:
  83. 3-1、数组上有5个元素,而某个链表上有3个元素,问此HashMap size 是多大?
  84. 3-1-1、只要是调用put() 方法添加元素,那么就会调用 ++size(这里有个例外是插入重复key的键值对,不会调用,但是重复key元素不会影响size),答案为7
  85. 3-2
  86. 4、.put()方法

3-1、HashMap-添加元素-.put()方法-执行示意图


容器(Map)-HashMap-源码分析 - 图4

3-2、.treeifyBin()方法

1、.treeifyBin()方法用于hashMap链表长度达到8以后转红黑树前的预处理,当链表长度达到8以后判断表长度是否大于63的判断就是在这个方法里完成的,若表长度小于64,则不再进一步处理,仅仅调用resize()方法重新调整表。 2、若表长度大于等于64,则开始进行预处理,把原来长度达到8的单向链表转换成双向链表的形式,双向链表头结点为TreeNode hd;转成双向链表以后调用链表树形化方法treeify()把链表树形化。 3、详细分析:HashMap之TreeNode(红黑树)源码分析

  1. /**
  2. TreeNode:树节点,代表红黑树的一个节点,一个Node相当于链表的一个节点,next属性对应着下一个节点
  3. TreeNode是红黑树的同时也是一个单项链表:TreeNode不仅是红黑树,还是链表。继承了LinkedHashMap.Entry,而Entry继承了 HashMap.Node
  4. */
  5. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  6. TreeNode<K,V> parent; // red-black tree links
  7. TreeNode<K,V> left;
  8. TreeNode<K,V> right;
  9. TreeNode<K,V> prev; // needed to unlink next upon deletion
  10. boolean red;
  11. TreeNode(int hash, K key, V val, Node<K,V> next) {
  12. super(hash, key, val, next);
  13. }
  14. }
  15. final void treeifyBin(Node<K,V>[] tab, int hash) {
  16. int n, index; Node<K,V> e;
  17. //判断表长度是否大于64,如果不大于64则调用resize()方法重新调整表大小
  18. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  19. resize();
  20. else if ((e = tab[index = (n - 1) & hash]) != null) {
  21. //定义树首节点、树尾节点
  22. TreeNode<K,V> hd = null, tl = null;
  23. //do.....while循环读取转换。直到链表最后一个元素
  24. do {
  25. //把当前节点转换为树节点
  26. TreeNode<K,V> p = replacementTreeNode(e, null);
  27. if (tl == null) //树尾节点为空,则表示链表没有根节点
  28. hd = p;//将首节点(根节点)指向当前节点,hd 双向链表头元素
  29. else {//尾节点不为空,则构成一个双向链表结构
  30. p.prev = tl;//将当前树节点的 前一个节点指向尾节点
  31. tl.next = p;//尾节点的后一个节点指向当前节点
  32. }
  33. tl = p;//把当前节点设为尾节点
  34. } while ((e = e.next) != null); //否则开始尝试把单向链表转换转成双向链表
  35. if ((tab[index] = hd) != null)//把表index索引所在位置桶替换成刚刚建立的双向链表,然后判断是否null
  36. //树形化: 调用treeify()方法树形化刚刚的hd开头的双向链表
  37. hd.treeify(tab);//调用treeify()方法树形化刚刚的hd开头的双向链表
  38. }
  39. }
  40. TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
  41. return new TreeNode<>(p.hash, p.key, p.value, next);
  42. }
  43. //最终调用了
  44. static class Entry<K,V> extends HashMap.Node<K,V> {
  45. Entry<K,V> before, after;
  46. Entry(int hash, K key, V value, Node<K,V> next) {
  47. super(hash, key, value, next);
  48. }
  49. }
  50. static class Node<K,V> implements Map.Entry<K,V> {
  51. final int hash;
  52. final K key;
  53. V value;
  54. Node<K,V> next;
  55. Node(int hash, K key, V value, Node<K,V> next) {
  56. this.hash = hash;
  57. this.key = key;
  58. this.value = value;
  59. this.next = next;
  60. }
  61. //......
  62. }

4、HashMap-存储机制-扩容方法

  1. 1、扩容:.resize(),HashMap 集合的元素已经大于了最大承载容量thresholdcapacity * loadFactor)。
  2. 2、扩容方法源码:JDK1.7jdk1.8之前是采用头插法(新链表与旧链表的顺序是反的)
  3. //参数 newCapacity 为新数组的大小
  4. void resize(int newCapacity) {
  5. Entry[] oldTable = table;//引用扩容前的 Entry 数组
  6. int oldCapacity = oldTable.length;
  7. if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的数组大小如果已经达到最大(2^30)了
  8. threshold = Integer.MAX_VALUE;///修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
  9. return;
  10. }
  11. Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组
  12. transfer(newTable, initHashSeedAsNeeded(newCapacity));//将数组元素转移到新数组里面
  13. table = newTable;
  14. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阈值
  15. }
  16. void transfer(Entry[] newTable, boolean rehash) {
  17. int newCapacity = newTable.length;
  18. for (Entry<K,V> e : table) {//遍历数组
  19. while(null != e) {
  20. Entry<K,V> next = e.next;
  21. //JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置
  22. if (rehash) {
  23. e.hash = null == e.key ? 0 : hash(e.key);
  24. }
  25. int i = indexFor(e.hash, newCapacity);//重新计算每个元素在数组中的索引位置
  26. e.next = newTable[i];//标记下一个元素,添加是链表头添加
  27. newTable[i] = e;//将元素放在数组上
  28. e = next;//访问下一个 Entry 链上的元素
  29. }
  30. }
  31. }
  32. 3、扩容方法源码:JDK1.8:在1.8后采用尾插法,同时1.8的链表长度如果大于8就会转变成红黑树。
  33. //1、如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。
  34. //2、前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。
  35. 3-1、源码分析:
  36. final Node<K,V>[] resize() {
  37. Node<K,V>[] oldTab = table;
  38. int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组如果为null,则长度赋值0
  39. int oldThr = threshold;
  40. int newCap, newThr = 0;
  41. if (oldCap > 0) { //如果原数组长度大于0
  42. if (oldCap >= MAXIMUM_CAPACITY) { //数组大小如果已经大于等于最大值(2^30)
  43. threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
  44. return oldTab;
  45. }
  46. //原数组长度大于等于初始化长度16,并且原数组长度扩大1倍也小于2^30次方
  47. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  48. oldCap >= DEFAULT_INITIAL_CAPACITY)
  49. newThr = oldThr << 1; // double threshold,阀值扩大1倍
  50. }
  51. //旧阀值大于0,则将新容量直接等于就阀值
  52. else if (oldThr > 0) // initial capacity was placed in threshold
  53. newCap = oldThr;
  54. else {//阀值等于0,oldCap也等于0(集合未进行初始化)
  55. newCap = DEFAULT_INITIAL_CAPACITY; //数组长度初始化为16
  56. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //阀值等于16*0.75=12
  57. }
  58. //计算新的阀值上限
  59. if (newThr == 0) {
  60. float ft = (float)newCap * loadFactor;
  61. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  62. (int)ft : Integer.MAX_VALUE);
  63. }
  64. threshold = newThr; //下次扩容的大小
  65. @SuppressWarnings({"rawtypes","unchecked"})
  66. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  67. table = newTab; //将当前表赋予table
  68. if (oldTab != null) { //若oldTab中有值需要通过循环将oldTab中的值保存到新表中
  69. for (int j = 0; j < oldCap; ++j) {
  70. Node<K,V> e;
  71. if ((e = oldTab[j]) != null) {
  72. oldTab[j] = null; //元数据j位置置为null,便于垃圾回收
  73. if (e.next == null) //数组没有下一个引用,说明这个node没有链表.
  74. //直接放在新表的e.hash & (newCap - 1)位置
  75. newTab[e.hash & (newCap - 1)] = e;
  76. else if (e instanceof TreeNode) //红黑树
  77. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  78. else { // preserve order
  79. Node<K,V> loHead = null, loTail = null; //存储与旧索引的相同的节点
  80. Node<K,V> hiHead = null, hiTail = null; //存储与新索引相同的节点
  81. Node<K,V> next;
  82. //获取新旧索引的节点
  83. do {
  84. next = e.next;//记录下一个结点
  85. //新表是旧表的两倍容量,实例上就把单链表拆分为两队,
  86. //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
  87. //原索引
  88. if ((e.hash & oldCap) == 0) {
  89. if (loTail == null)
  90. loHead = e;
  91. else
  92. loTail.next = e;
  93. loTail = e;
  94. }
  95. //原索引+oldCap
  96. else {
  97. if (hiTail == null)
  98. hiHead = e;
  99. else
  100. hiTail.next = e;
  101. hiTail = e;
  102. }
  103. } while ((e = next) != null);
  104. //原索引放到新表bucket里
  105. if (loTail != null) {
  106. loTail.next = null;
  107. newTab[j] = loHead;
  108. }
  109. //原索引+oldCap放到bucket里
  110. if (hiTail != null) {
  111. hiTail.next = null;
  112. newTab[j + oldCap] = hiHead;
  113. }
  114. }
  115. }
  116. }
  117. }
  118. //返回新表
  119. return newTab;
  120. }
  121. 3-2、.resize()方法执行过程:
  122. 1.判定数组是否已达到极限大小,若判定成功将不再扩容,直接将老表返回
  123. 2.若新表大小(oldCap2)小于数组极限大小&老表大于等于数组初始化大小 判定成功则 旧数组大小oldThr 经二进制运算向左位移1个位置 oldThr2当作新数组的大小
  124. 2.1. 若[2]的判定不成功,则继续判定 oldThr (代表 老表的下一次扩容量)大于0,若判定成功 则将oldThr赋给newCap作为新表的容量
  125. 2.2 [2] 和[2.1]判定都失败,则走默认赋值 代表 表为初次创建
  126. 3.确定下一次表的扩容量, 将新表赋予当前表
  127. 4.通过for循环将老表中德值存入扩容后的新表中
  128. 4.1 获取旧表中指定索引下的Node对象 赋予e 并将旧表中的索引位置数据置空
  129. 4.2 e的下面没有其他节点则将e直接赋到新表中的索引位置
  130. 4.3 e的类型为TreeNode红黑树类型
  131. 4.3.1 分割树,将新表和旧表分割成两个树,并判断索引处节点的长度是否需要转换成红黑树放入新表存储
  132. 4.3.2 通过Do循环 不断获取新旧索引的节点
  133. 4.3.3 通过判定将旧数据和新数据存储到新表指定的位置
  134. 5.最后返回值为 扩容后的新表。

5、HashMap-存储机制-查找元素

  1. 1、查找元素-通过key查找value: 通过 key 找到计算索引,找到桶位置,先检查第一个节点,如果是则返回,如果不是,则遍历其后面的链表或者红黑树。其余情况全部返回 null
  2. 1-1、源码分析:
  3. public V get(Object key) {
  4. Node<K,V> e;
  5. return (e = getNode(hash(key), key)) == null ? null : e.value;
  6. }
  7. //获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可。
  8. final Node<K,V> getNode(int hash, Object key) {
  9. Node<K,V>[] tab; /Entry对象数组
  10. Node<K,V> first, e; //在tab数组中经过散列的第一个位置
  11. int n; K k;
  12. // 找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]
  13. if ((tab = table) != null && (n = tab.length) > 0 &&
  14. (first = tab[(n - 1) & hash]) != null) {
  15. //根据key计算的索引检查第一个索引: 判断条件是hash值要相同,key值要相同
  16. if (first.hash == hash && // always check first node
  17. ((k = first.key) == key || (key != null && key.equals(k))))
  18. //匹配key值成功,则返回该值
  19. return first;
  20. //不是第一个节点,则继续检查first后面的节点
  21. if ((e = first.next) != null) {
  22. if (first instanceof TreeNode)//遍历树查找元素,并返回
  23. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  24. //若上面判定不成功 则认为下一个节点为单向链表,通过循环匹配值
  25. do {
  26. //遍历链表查找元素,找到key值和hash值都相同的Node
  27. if (e.hash == hash &&
  28. ((k = e.key) == key || (key != null && key.equals(k))))
  29. //匹配成功后返回该值
  30. return e;
  31. } while ((e = e.next) != null);
  32. }
  33. }
  34. return null;
  35. }
  36. 1-2、.get()方法执行过程:
  37. 1-2-1.判定三个条件 table不为Null & table的长度大于0 & table指定的索引值不为Null
  38. 1-2-2.判定 匹配hash & 匹配key 成功则返回 该值
  39. 1-2-3. first节点的下一个节点不为Null
  40. 1-2-3-1 first的类型为TreeNode 红黑树 通过红黑树查找匹配值 并返回查询值。
  41. 1-2-3-2 若上面判定不成功 则认为下一个节点为单向链表,通过循环匹配值。
  42. 1-3、.get()方法-小结:
  43. 1-3-1、首先通过hash()函数得到对应bucket的下标,然后依次遍历冲突链表,通过key.equals(k)方法来判断是否是要找的那个entry
  44. 1-3-2hash(k)&(table.length-1)等价于hash(k)%table.length,原因是HashMap要求table.length必须是2的指数,因此table.length-1就是二进制低位全是1,跟hash(k)相与会将哈希值的高位全抹掉,剩下的就是余数了。
  45. 2、查找元素-判断是否存在给定的 key 或者 value
  46. public boolean containsKey(Object key) {
  47. return getNode(hash(key), key) != null;
  48. }
  49. public boolean containsValue(Object value) {
  50. Node<K,V>[] tab; V v;
  51. if ((tab = table) != null && size > 0) {
  52. //遍历桶,
  53. for (int i = 0; i < tab.length; ++i) {
  54. //遍历桶中的每个节点元素
  55. for (Node<K,V> e = tab[i]; e != null; e = e.next) {
  56. if ((v = e.value) == value ||
  57. (value != null && value.equals(v)))
  58. return true;
  59. }
  60. }
  61. }
  62. return false;
  63. }

6、HashMap-存储机制-删除元素

  1. 1HashMap 删除元素首先是要找到 桶的位置,然后如果是链表,则进行链表遍历,找到需要删除的元素后,进行删除;如果是红黑树,也是进行树的遍历,找到元素删除后,进行平衡调节,注意,当红黑树的节点数小于 6 时,会转化成链表。
  2. 2、源码分析:
  3. public V remove(Object key) {
  4. Node<K,V> e;
  5. return (e = removeNode(hash(key), key, null, false, true)) == null ?
  6. null : e.value;
  7. }
  8. final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
  9. Node<K,V>[] tab; Node<K,V> p; int n, index;
  10. //(n - 1) & hash找到桶的位置
  11. if ((tab = table) != null && (n = tab.length) > 0 &&
  12. (p = tab[index = (n - 1) & hash]) != null) {
  13. Node<K,V> node = null, e; K k; V v;
  14. //如果键的值与链表第一个节点相等,则将 node 指向该节点
  15. if (p.hash == hash &&
  16. ((k = p.key) == key || (key != null && key.equals(k))))
  17. node = p;
  18. //如果桶节点存在下一个节点
  19. else if ((e = p.next) != null) {
  20. //节点为红黑树
  21. if (p instanceof TreeNode)
  22. node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//找到需要删除的红黑树节点
  23. else {
  24. do {//遍历链表,找到待删除的节点
  25. if (e.hash == hash &&
  26. ((k = e.key) == key ||
  27. (key != null && key.equals(k)))) {
  28. node = e;
  29. break;
  30. }
  31. p = e;
  32. } while ((e = e.next) != null);
  33. }
  34. }
  35. //删除节点,并进行调节红黑树平衡
  36. if (node != null && (!matchValue || (v = node.value) == value ||
  37. (value != null && value.equals(v)))) {
  38. if (node instanceof TreeNode)
  39. ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
  40. else if (node == p)
  41. tab[index] = node.next;
  42. else
  43. p.next = node.next;
  44. ++modCount;
  45. --size;
  46. //为实现 LinkedHashMap 做准备的,是一个空方法实现,在 LinkedHashMap 中进行了重写,用来维护删除节点后,链表的前后关系。
  47. afterNodeRemoval(node);
  48. return node;
  49. }
  50. }
  51. return null;
  52. }

7、HashMap-线程安全性-分析

  1. 1、为什么说HashMap是线程不安全的?
  2. 1-1、在并发的多线程使用场景中使用HashMap可能造成死循环。
  3. 1-2、举例Demo: 可断点调试
  4. public class HashMapInfiniteLoop {
  5. //map初始化为一个长度为2的数组,loadFactor=0.75,threshold=2*0.75=1,也就是说当put第二个key的时候,map就需要进行resize。
  6. private static HashMap<Integer,String> map = new HashMap<Integer,String>(20.75f);
  7. public static void main(String[] args) {
  8. map.put(5 "C");
  9. new Thread("Thread1") {
  10. public void run() {
  11. map.put(7, "B");
  12. System.out.println(map);
  13. };
  14. }.start();
  15. new Thread("Thread2") {
  16. public void run() {
  17. map.put(3, "A);
  18. System.out.println(map);
  19. };
  20. }.start();
  21. }
  22. }

8、HashMap-其他方法-分析-.getOrDefault()方法

  1. 1、.getOrDefault()方法:jdk1.8引入,如果在Map中存在key,则返回key所对应的的value。如果 Map中不存在key,则返回默认值。
  2. 2、源代码:
  3. // Map.getOrDefault(key,默认值);
  4. public V getOrDefault(Object key, V defaultValue) {
  5. Node<K,V> e;
  6. return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
  7. }
  8. 3、举例Demo
  9. .getOrDefault(Object key, V defaultValue)
  10. public class Demo {
  11. public static void main(String[] args) {
  12. Map<String, Integer> map = new HashMap<>();
  13. map.put("张三", 23);
  14. map.put("赵四", 24);
  15. map.put("王五", 25);
  16. String age= map.getOrDefault("赵四", 30);
  17. System.out.println(age);// 24,map中存在"赵四",使用其对应值24
  18. String age = map.getOrDefault("刘能", 30);
  19. System.out.println(age);// 30,map中不存在"刘能",使用默认值30
  20. }
  21. }
  22. 4、.getOrDefault(K V)方法中.getNode()方法,源代码:
  23. final HashMap.Node<K,V> getNode(int hash, Object key) {
  24. //定义变量
  25. HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
  26. //查看数据需要满足一下条件
  27. //1)数组不为空
  28. //2)数组长度>0
  29. //3)通过hash计算出该元素在数组中存放位置的索引,而且该索引处数据不为空null
  30. if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
  31. //判断该数组索引位置处第一个是否为我们要找的元素 判断条件需要满足hash 和 key 相同
  32. // always check first node
  33. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
  34. //如果第一个就是我们要找的,直接返回即可
  35. return first;
  36. //如果第一个不是,我们需要循环遍历,然后找数据
  37. if ((e = first.next) != null) {
  38. //如果第1个的元素是红黑树类型的节点
  39. if (first instanceof HashMap.TreeNode)
  40. //那我们需要调用红黑树的方法查找节点
  41. return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
  42. //如果不是,则该为链表,需要遍历查找
  43. do {
  44. //循环判断下一个节点的hash和key是否相同
  45. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
  46. return e;
  47. //更新e为下一个
  48. } while ((e = e.next) != null);
  49. }
  50. }
  51. //没找到返回Null
  52. return null;
  53. }
  1. https://www.cnblogs.com/little-fly/p/7344285.html
  2. https://segmentfault.com/a/1190000018520768
  3. https://www.cnblogs.com/ysocean/p/8711071.html
  4. https://www.jianshu.com/p/003256ce41ce
  5. https://blog.csdn.net/qq_40574571/article/details/97612100
  6. https://www.oracle.com/cn/database/technology/maps.html
  7. https://www.cnblogs.com/mkfywj/p/5543600.html
  1. Float.isNaN(loadFactor)
  2. 遍历树查找元素:.getTreeNode(hash, key)
  3. 如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办? 也就是说,
  4. 当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,
  5. 发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。 前面我们提到过,
  6. 哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。
  7. 但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突,
  8. 那么冲突如何解决呢?哈希冲突的解决方案有多种。 开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),
  9. 再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
  10. https://www.yuque.com/lius/java/funwrx
  11. https://www.yuque.com/moercheng/pkgur7/pgm06a
  12. https://www.yuque.com/moercheng/pkgur7/cxu7md/edit#10.1.HashMap--1.7
  13. https://www.yuque.com/moercheng/pkgur7/pwg92x/edit#fdc5b2e7