HashMap总结

1,085 阅读7分钟

数据结构

image.png

image.png

重要参数

  • **容量(**capacity): HashMap中数组的长度
    • 容量范围:必须是2的幂 & <最大容量(2的30次方)
    • 初始容量 = 哈希表创建时的容量
    • 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    • 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换) static final int MAXIMUM_CAPACITY = 1 << 30;
  • 负载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
    • 默认加载因子 = 0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)

image.png

源码分析

构造函数

image.png

put方法解析

image.png

  1. 若哈希表未初始化(即 table为空) 则使用 构造函数时设置的阈值(即初始容量) 初始化 数组table
  2. 判断key是否为空值null,
    • 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0] 本质:key = Null时,hash值 = 0,故存放到table[0]中)
    • 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)
    • 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
      • 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
      • 若该key不存在,则将“key-value”添加到table中
  • HashMap的键key 可为null(区别于 HashTable的key 不可为null)
  • HashMap的键key 可为null且只能为1个,但值value可为null且为多个

初始化哈希表

将传入的容量大小转化为:>传入容量大小的最小的2的幂,容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:>传入容量大小的最小的2的次幂

如何计算hashTable中的索引

image.png

  • 计算hashCode
  • 二次扰动:位运算和异或运算处理hashCode => h
  • index = h & (length-1)

为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

结论:容易出现 哈希码 与 数组大小范围不匹配的情况,即 计算出来的哈希码可能 不在数组大小范围内,从而导致无法匹配存储位置

image.png

为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突。

为什么数组的长度要求为2的n次幂

数组长度减1的结果二进制形式为 011111...形式 首位为0 后面均为1 保证 index = h&(n-1) 的结果的奇偶性由 h本身的奇偶性决定,最后一位为0,则存储位肯定是偶数增大了冲突的概率

键值对的添加/替换流程

  • 添加使用的数组头插法,新添加的节点总是放在数组的头位置
  • 替换发现key相等只用找到key对应的节点把value覆盖掉就行

扩容机制

put方法时判断数组实际大小大于扩容阈值,保存旧数组创建两倍当前容量的数组,遍历旧数组的每个数据,重新根据hash函数计算,将旧数组上的每个数据逐个转移到新数组中,重新计算新数组的阈值,扩容结束。

1.7和1.8的区别

image.png

image.png

HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

答:底层对于hashMap的key值的hashCode结合数组长度进行hash操作,操作有无符号右移、按位异或、按位与来计算出存储的索引。 hash函数还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。。

当两个对象的hashcode相等时会怎么样?

答:会产生哈希碰撞。若key值内容相同则替换旧的value,否则就连接到链表后面,如果链表长度超过阈值8,且数组超过临界值会先进行扩容,如果数组长度超过64,且链表长度超过8,就转换为红黑树存储。

HashMap的扩容机制

首先数组容量是有限的,当达到临界值的时候就会进行扩容,也就是resize 临界值=数组当前的长度 x 负载因子 默认是 16 x 0.75 = 12 扩容分为两步,首先创建一个长度是原来数组两倍的空数组,然后把原来数组中的所有数据重新hash到新的数组中 注:数组长度变化了hash值也会随之改变, Hash的公式—> index = HashCode(Key) & (Length - 1)

负载因子为什么是0.75

HashMap有一个初始容量大小,默认是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 为了减少冲突的概率,当hashMap的数组长度到了一个临界值就会触发扩容,把所有元素rehash再放到扩容后的容器中,这是一个非常耗时的操作。

而这个临界值由【加载因子】和当前容器的容量大小来确定:DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR ,即默认情况下是16x0.75=12时,就会触发扩容操作。 理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。 从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。

为什么hashMap底层的数组长度必须是2的n次幂

这个和hash算法的公式有关,为了极可能的保证hash算法结果的均匀分布,如果是2的n次幂的话,length-1的二进制就全是1,那么index的结果就等同于hashcode的后几位的值了,只要保证得到的hashCode分布均匀那么index就会分布均匀。

为什么我们重写equals方法的时候需要重写hashcode方法

如果为重写的话,object中实现的equal是比较的两个对象的内存地址,而我们hashMap中equals在链表中找到具体的key 重写hashcode就是为了保证不同的对象返回不同的hash值,相同的对象返回相同的hash值 在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样 对于值对象,== 比较的是两个对象的值 对于引用对象, == 比较的是两个对象的地址 大家是否还记得我说的HashMap是通过key的hashCode去寻找index的,那index一样就形成链表了,也就是说”帅丙“和”丙帅“的index都可能是

HashMap线程不安全如何解决

线程安全的有HashTable、ConcurrentHashMap、SynchronizedMap,性能最好的是ConcurrentHashMap,一般都用它来解决。 Hashtable线程安全但效率低下 Hashtable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap并发机制

1.7中采用分段锁的方式来保证线程安全,每一把锁用于锁住数组中一部分数据,这样就可以解决多线程访问容器中不同数据段数据不需要竞争锁的问题,有效提高了并发效率 1.8中采用了CAS+synchronized的方式保证线程安全,数据结构采用:数组+链表+红黑树。CAS(比较并交换的原子操作) V O N 内存值 旧值 新值 取内存地址中的值与旧值比较,如果相同,则表示无线程占用将N(新值)写入进去。】

红黑树原理是什么?

红黑树是一个含有红黑节点的平衡二叉树,每个节点不是红色就是黑色,根节点和叶子节点都是黑色,且每个红色结点的两个子结点一定都是黑色。需要通过左旋、右旋、上色来将链表转换成红黑树,主要用于提高链表中的查找效率,牺牲了插入和删除的效率,每次插入和删除都需要再进行调整恢复红黑树。 与平衡二叉树区别 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长 ———————————————— 版权声明:本文为CSDN博主「doubleStrongWu」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/qq_37126175…