HashMap源码深度剖析 2

简介: HashMap源码深度剖析

3.4 HashMap扩容方法

目标:图解+代码(map扩容与数据迁移)

注意:扩容复杂、绕、难

图解前提: 按8和16的长度讲(图片不容易展示)

迁移前:长度8 扩容临界点6(8*0.75)

9290cf1a975e49a0a4a8d46acee70b63.png

迁移过程

3c5e18c3878b45908ceb289c70832ce2.pnge15e293f27874a5ba37d7f8d1649c2aa.png


核心源码resize方法

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 &&//扩容2倍:oldCap
<< 1,左移1位,相当于oldCap乘以2的1次方
          oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // 扩容2倍:将阈值threshold*2得到新的阈值
   }
    else if (oldThr > 0) // HashMap(int initialCapacity, float loadFactor)调
      newCap = oldThr;
    else {        // zero initial threshold signifies using defaults
      newCap = DEFAULT_INITIAL_CAPACITY;//第一次
put!!!!!!!!!!!!!!!!!!
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩充
阈值
   }
    if (newThr == 0) {//如果新阈值为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;//gc处理
          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;//低位链表(原位置i)
            Node<K,V> hiHead = null, hiTail = null;//高位链表(i+n位
置)
            Node<K,V> next;
            do {
              next = e.next;
              if ((e.hash & oldCap) == 0) {// 如果为0,元素位置在扩容后
数组中的位置没有发生改变(低位)
                if (loTail == null)
                  loHead = e;// 头节点
                else
                  loTail.next = e;
                loTail = e;
             }
              else {//不为0,元素位置在扩容后数组中的位置发生了改变,新的下
标位置是(原下标位置+原数组长)
                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;//返回新数组
 }

总结(扩容与迁移):

1、扩容就是将旧表的数据迁移到新表

2、迁移过去的值需要重新计算hashCode,也就是他的存储位置

3、关于位置可以这样理解:比如旧表的长度8、新表长度16

旧表位置4有6个数据,假如前三个hashCode是一样的,后面的三个hashCode是一样的

迁移的时候;就需要计算这6个值的存储位置

4、如何计算位置?采用低位链表和高位链表;如果位置4下面的数据e.hash & oldCap等于0,

那么它对应的就是低位链表,也就是数据位置不变

e.hash & oldCap不等于0呢?就要重写计算他的位置也就是j + oldCap(4+8);这个12,就是高位链表

位置(新数组12位置)


3.5 HashMap获取方法

目标:图解+断点分析get源码

获取流程

6896d6b4ebad4709a8f18749efbed170.png

get主方法

  public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;//先计算
hashcode
 }

getNode方法

  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 && // 数组查找!!!!!
       ((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;
 }

总结:

查询思路比较简单,如果是数组直接返回、如果是红黑实例,就去树上去找,最后,去做链表循环查找


3.6 HashMap移除方法

目标:图解+断点分析remove源码

移除流程

37ac921b84ec40d4ae109584aa784ecc.png

tips:

两个移除方法,参数上的区别

走的同一个代码

移除方法:一个参数

 public V remove(Object key) {
    Node<K,V> e; 定义一个节点变量,用来存储要被删除的节点(键值对
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
      null : e.value;
 }

移除方法:二个参数

  @Override
  public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
 }

核心方法removeNode

  /**
  * Implements Map.remove and related methods
  *
  * @param hash 扰动后的hash值
  * @param key 要删除的键值对的key,要删除的键值对的value,该值是否作为删除的条件取决于
matchValue是否为true
  * @param value key对应的值
  * @param matchValue 为true,则当key对应的值和equals(value)为true时才删除;否则不关
心value的值
  * @param movable 删除后是否移动节点,如果为false,则不移动
  * @return 返回被删除的节点对象,如果没有删除任何节点则返回null
  */
  final Node<K,V> removeNode(int hash, Object key, Object value,
               boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
     (p = tab[index = (n - 1) & hash]) != null) {
      Node<K,V> node = null, e; K k; V v;
      if (p.hash == hash &&
       ((k = p.key) == key || (key != null && key.equals(k))))
        node = p;//寻址后的值给node
      else if ((e = p.next) != null) {
        if (p instanceof TreeNode)
          node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//红黑查找
        else {
          do { //链表查找
            if (e.hash == hash &&
             ((k = e.key) == key ||
              (key != null && key.equals(k)))) {
              node = e;
              break;
           }
            p = e;// 走到这里,说明e也没有匹配上,P-->E;表示p是e的父节点
         } while ((e = e.next) != null);//如果e存在下一个节点,那么继续去匹
配下一个节点
       }
     }//如果node不为空,说明根据key匹配到了要删除的节点
      if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
        if (node instanceof TreeNode)
         ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//红
黑删除
        else if (node == p)// 如果该节点不是TreeNode对象,node == p 的意思是该
node节点就是首节点
          tab[index] = node.next;//由于删除的是首节点,那么直接将节点数组对应
位置指向到第二个节点即可
        else
          p.next = node.next; 如果node节点不是首节点,此时p是node的父节
点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
        ++modCount;//HashMap的修改次数递增
        --size;// HashMap的元素个数递减
        afterNodeRemoval(node);//子类扩展
        return node;//返回删除后的节点
     }
   }
   return null;//找不到删除的node,返回null
 }

总结:

移除和查询路线差不多,找到后直接remove

注意他的返回值,是删除的那个节点的值


4 哈希最常见面试题

4.1 为什么要从JDK1.8之前的链表设计,修改为链表或红黑树的设计?

当某个链表比较长的时候,查找效率还是会降低。

如果table[index]的链表的节点的个数比较少,(8个或以内),就保持链表。如果超过8个,那么

就要考虑把链表转为一棵红黑树。


4.2 什么时候树化?

table[index]下的结点数一达到8个就树化吗?

如果table[index]的节点数量已经达到8个了,还要判断table.length是否达到64,如果没有达到

64,先扩容


4.3 什么时候从树–>链表

当你删除结点时,这棵树的结点个数少于6个,会反树化,变为链表


4.4 初始容量是16,为什么是2的指数次幂?

1、方便计算

2、扩容与数据迁移(重点)

因为2的幂次方在二进制的表达中可以保证后几位均为1,为什么要这样做呢?举一个例子就很清楚了,

基于按位与操作的思想,两数都为1才为一,若数组长度为15,(15-1)即14表示的话就是1110,

1110&1110和1110&1111的结果是相同的,这不就增加了哈希碰撞的概率了,因此,保持n-1的后几位

为1,就可以根据hash值的后几位来判别应该放在哪个桶里了


4.5 为什么加载因子是0.75呢?

其实是通过泊松分布来计算的

选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择

总结:

通过统计学和概率计算得出来的结果


4.6 什么是哈希冲突(碰撞)?,如何解决

如果两个不同的元素(key、value、hash不同),通过哈希函数得出的实际存储地址相同

也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被

其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞

该如何解决呢?链表


4.7 获取hash 值,为什么不用hashCode直接取呢?左移、右移和异或怎么计算?

让hash分布、散列更加均匀,减少hash碰撞的概率。

tips:(h = key.hashCode()) ^ (h >>> 16)

是如何计算的?

异或:右移

举例:

key.hashCode() int转32二进制为0000010101001010 0101001011110001

右移16位,高位不足进行补零(直接在前面加16个0)

0000010101001010 0101001011110001(移动前)

0000000000000000 0101001011110001 (移动后)

开始异或(相同为0.不同为1)

0000010101001010 0101001011110001

0000000000000000 0101001011110001

异或后的(相同为0,不同为1)

0000010101001010 0000000000000000


关于左移

59cc65fe3fcc4b458c429c4dbfffb334.png

2^0:在二进制中表示1

29a48b3c75154703a9441970511523ac.png

注意:上面前导为1,计算出来的就是负数


关于右移

ff2ceccc22d94245800c4798b6a15d26.png

tips

以上没有按照实际32位作图,只是举例,图片放不下


4.8 JDK1.8相比1.7主要优化的地方?

jdk1.8是链表+数组+红黑树;

jdk1.7链表采用头插法,头插法比较快,但容易造成死循环;jdk1.8是尾插法;


5 HashMap常问面试题

HashMap常问面试题


目录
相关文章
|
2月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
32 2
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
41 2
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
65 0
|
1天前
HashMap源码浅分析与解读
阿华代码解读,不是逆风就是你疯HashMap 和TreeMap都继承于Map,Map是一个接口在实现这个接口的时候,需要实例化TreeMap或者HashMap。
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
64 5
|
2月前
|
算法 索引
让星星⭐月亮告诉你,HashMap的resize()即扩容方法源码解读(已重新完善,如有不足之处,欢迎指正~)
`HashMap`的`resize()`方法主要用于数组扩容,包括初始化或加倍数组容量。该方法首先计算新的数组容量和扩容阈值,然后创建新数组。接着,旧数组中的数据根据`(e.hash & oldCap)`是否等于0被重新分配到新数组中,分为低位区和高位区两个链表,确保数据迁移时的正确性和高效性。
70 3
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap中红黑树TreeNode的split方法源码解读
本文详细解析了Java中`HashMap`的`TreeNode`类的`split`方法,该方法主要用于在`HashMap`扩容时将红黑树节点从旧数组迁移到新数组,并根据`(e.hash & oldCap)`的结果将节点分为低位和高位两个子树。低位子树如果元素数少于等于6,则进行去树化操作;若多于6且高位子树非空,则进行树化操作,确保数据结构的高效性。文中还介绍了`untreeify`和`replacementNode`方法,分别用于将红黑树节点转换为普通链表节点。
29 2
|
2月前
|
存储 Java
HashMap之链表转红黑树(树化 )-treefyBin方法源码解读(所有涉及到的方法均有详细解读,欢迎指正)
本文详细解析了Java HashMap中链表转红黑树的机制,包括树化条件(链表长度达8且数组长度≥64)及转换流程,确保高效处理大量数据。
109 1
|
2月前
|
存储 缓存 Java
HashMap源码解析
HashMap源码解析
|
6月前
|
存储 安全 Java
《ArrayList & HashMap 源码类基础面试题》面试官们最喜欢问的ArrayList & HashMap源码类初级问,你都会了?
《ArrayList & HashMap 源码类基础面试题》面试官们最喜欢问的ArrayList & HashMap源码类初级问,你都会了?
44 0