笔记总结-Java集合

本文详细介绍了Java HashMap的工作原理,包括容量必须为2的幂、扩容边界值、put方法的实现、哈希值计算、扩容与树化的时机,以及解决哈希冲突的策略。对比了JDK1.7与1.8的区别,讨论了为何不直接使用红黑树以及加载因子设为0.75的原因。同时,提到了ConcurrentHashMap与HashMap的异同,强调了HashMap的线程不安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、hashmap的数组容量一定是2的几次幂

  1. 在new Hahmap()的时候如果不传递参数,默认的初始化容量就是16
  2. 如果传递参数,比如传递28,那initcapacity就是28,会走这段代码:(每次左移1位,都乘以2,说明就是2的n次幂)
    int capacity=1;
    while(capacity<initCapacity)
    capacity<<=1;

2、hashmap扩容的边界值是12

  1. Math.min(capacity装载因子,数组的最大容量+1)求最小值的结果就是160.75=12
  2. 当数组>12时,数组扩容

3、hashmap存储数据的put方法

  1. 通过key值获取哈希码h^=k.hascode
  2. 根据哈希拿到元素在数组的位置h&(table.length-1)相当于h%length
  3. 添加元素的方法addEntry(当未到达阈值会createEntry,到达阈值会将数组扩容成原来的2倍)
  4. 当出现哈希碰撞时,7上8下(jdk1.7插入到当前元素之前,当jdk1.8之后会插入到当前元素的后面)(多线程同时操作及逆行扩容时,可能会有循环链表的出现)
  5. 当出现哈希碰撞时,先比较哈希hash再比较equals(key)值

4、hashmap中计算key的哈希值

jdk1.7

  1. 通过key获得哈希码的时候,没有直接hascode拿到哈希值
  2. 而是通过二次散列(通过一个异或的运算h^=k.hascode)
  3. 核心思想是增加哈希码的不确定性,不会轻易的出现哈希碰撞
    h=(h>>>20)(h>>12)等等

jdk1.8时:
4. h=key.hascode()^(h>>>16)也是为了让高16位和低16位同时参与运算,让数据散列更加均匀

5、 扩容的时机

  1. 第一次put时,数组的长度为null,也会resize扩容
  2. 数组的长度大于容量*加载因子

  1. 扩容的大小是左移一位,原来的2倍

6、树化的时机

  1. 容量大于等于64(当数组的容量没有达到64,会优先选择扩容而不是树化)
  2. 链表的长度大于等于8

  1. 为什么大于8的时候会树化?

7、解决哈希冲突的几种办法:

  1. 再散列法:通过H(key)拿到哈希值出现哈希碰撞后,以这次的H(key)为基础,再次hash,依次类推,直到不出现哈希碰撞为止,再哈希法的哈希表的长度要远远大于需要存放的元素,因为要再哈希,又不能把之前的H(key)删掉,只能做删除标记,不能真正删除结点
  2. 再哈希法:提供了多种hash方法,第一种hash不行,换第二种,直到找到不出现结点碰撞的哈希方法为止,有点浪费时间
  3. 拉链法:hashmap就是采用的这种方法,把哈希值相同的放到一个单链表中,把链表的头指针放到对应的哈希表的槽位上

8、树结点:父节点,左节点,右节点,颜色等;链表节点:下一个结点

  1. 所以树节点的大小是链表节点的2倍左右,一般不会树化

9、JDK1.8和1.7之间的区别

  1. JDK1.8新增加了红黑树
  2. JDK1.8从头插变成了尾插

10、hashmap是一个集合容器,用来以键值对的形式进行存储的

  1. 在JDK1.7和JDK1.8使用的底层数据结构是不一样的:
  2. JDK1.7使用的是哈希表:数组+链表;JDK1.8使用的是数组+链表+红黑树(红黑树的引入是为了提高查询效率:链表查询的时间复杂度是O(n),红黑树的时间复杂度是O(logn)当链表过长时,会影响hashmap的性能,所以jdk1.8做了优化)
  3. 当链表到达一定条件时才会树化:一个是当链表的长度大于8时会树化,另一个是当数组容量大于64时,会树化,当没有64时会选择数组扩容
  4. 再一个是jdk1.7在链表中添加数据时采用的是头插法,jdk1.8换成了尾插法(头插法会有一些问题的产生:循环链表和丢失数据等)

  1. hashmap的实现原理:在创建hashamap的时候,阿里规约里要求我们传入一个初始化容量,这个初始化容量最好是2的次幂(初始化容量的默认值是16,数据会从1开始左移,左移到最近一个大于到你传入的数值,如果传入28,最近一个2次幂是32)
  2. 初始化之后使用put添加数据,根据key的哈希值&(数组长度-1)就可以拿到存储的数组下标(用&代替了原来的%运算,效率更高)
  3. 拿到存储的下标位置后,进行存值,当没有哈希碰撞时,直接addEntry存储进去
  4. 当出现哈希碰撞时,先比较hash再比较equals(key)值
  5. 在链表节点存储数据时,JDK1.7使用的是头插法,JDK1.8使用的是尾插法
  6. 插入数据还会有树化和扩容的问题出现:
  7. 树化:当数组容量大于64,当链表长度大于8时会树化(树化要将链表左旋右旋,改变颜色,而且树节点的大小差不都是链表结点的两倍,所以一般能不树化就不树化)
  8. 扩容:当数组的容量大于容量*加载因子时也就是12时会扩容,一般都是扩容到原来的2倍,源码当中好像是左移了1位
  9. 红黑树的特点:根节点是黑色的,相邻节点的颜色不能相同,从根节点到子节点经过的路径的黑节点的个数必须相同

11、为什么链表的长度大于8时会树化

  1. 因为在数组的链表上的结点遵循泊松分布的规则
  2. 当结点出现越多,相同结点出现的概率越小
  3. 当结点的个数是8时,相同结点出现的概率是0.00006,非常小
  4. 基本不会到达树化的情况,而且尽量不要树化,因为要把一个链表转为红黑树,要左旋右旋,颜色设置,树化是很消耗性能的

12、头插法会产生的问题

  1. 当多线程操作时,会形成循环链表,形成死循环,因此JDK1.8使用尾插法插入数据,在扩容的时候保持链表原有的顺序

13、为什么加载因子是0.75

  1. 是哈希碰撞和空间利用率的一个折中
  2. 如果加载因子是1,空间利用率很高,但是容易发生碰撞
  3. 如果加载因子是0.5,虽然不易发生碰撞,但是空间利用率低

14、为什么哈希冲突时,不直接使用红黑树,而是使用链表

  1. 本来jdk1.8引入红黑树就是为了解决链表过长,造成hashmap性能差
  2. 本来在链表的长度小于8的时候,能够满足检索效率
  3. 当链表过长时,红黑树的时间复杂度是O(logn),链表的时间复杂度是O(n),使用红黑树才是更好的选择
  4. 但是一般不建议树化,而且树化的条件一般也不容易达到:数组容量达到64,链表的长度位8

15、hashmap为什么是线程不安全的

  1. 在JDK1.7中,插入元素使用的是头插法,但是当多个线程并发操作时,就可能有循环链表的出现,造成死循环,所以在jdk1.8使用的是尾插法
  2. 多线程操作时,造成元素丢失,当计算出来的索引一样时,那么前一个key就会倍下一个key覆盖掉,造成元素丢失
  3. 多线程中,get和put同时并发操作时,put的时候需要扩容,那么get的数据就为空

currentHashMap

1、 jdk1.8的初始化容量和hashmap计算方式是不一样的

  1. hashmap中的初始化容量是距离最近的你传入的那个值,如果传入的是28,那初始化容量就是32
  2. currenthashmap的容量是传入的初始化容量(init+(init>>>1)+1),然后就是距离最近的一个二次幂值,如果传入的是28,那么初始化容量就是28+14+1=43,也就是距离43最近的一个二次幂也就是64

2、构造方法中有一个sizeCtl变量,当值不同时代表的意义不同

  1. sizeCtl为0时,表示数组未被初始化,数组的初始化容量就是16
  2. sizeCtl为正数时,表示被初始化后,扩容的阈值
  3. sizeCtl为-1时,表示正在初始化
  4. sizeCtl为-N时,表示正在扩容操作

3、

  1. currentHashMap不允许空值空键,HashMap允许空值空键
  2. BST(B+树),AVL(平衡二叉树),RBT(红黑树)
  3. 调用put方法添加数据时,不允许空值空键,根据key拿到哈希值,并进行一定的扰动,让哈希散列均匀,然后调用init初始化方法,里有两个判断,是否sizeCtl<0,表示正在有线程在做初始化,那就Thread.yield让出CPU的执行权,如果sizeCtl>0表示,就CAS自旋把当前线程的sizeCtl设置成-1,表示当前线程在做初始化,使用DCL(判断了两次table是否为空)的方式new Node,然后将sizeCtl<<2变成原来的0.75倍,siztCtl表示已经初始化完成,当前值是扩容阈值
  4. 在初始化数组时,并发操作时使用CAS来确保当前时刻只有一个线程做并发的执行,这里用到了一个全局变量sizeCtl将sizeCtl置为-1,表示当前线程正在初始化操作,让别的线程Thread.yeild让出CPU时间片
  5. 用到了CAS和DCL,当前结点.hascode>0,说明是链表,否则就是数结构
  6. concurrentHashMap,HashTable
  7. 转义-中文编码解码,不太好,可以用二进制

4、 concurrent Hash Map和hashmap思路都是一样的都继承了abstractmap,大致的方法都是一样的,红黑树的底层实现不太一样

  1. concurrentHashMap是如何保证线程安全的?
    put方法中:数组init初始化,先通过CAS加设置sizeCtl的值,保证当前只有一个线程会进行数组初始化,其他线程会遇到sizeCtl=-1表示有线程正在初始化,就会Thread.yield让出CPU的执行权,
    put方法中:哈希冲突时,使用sych加锁,保证只有一个线程操作
    红黑树旋转时:锁住根节点
    数组中的槽点拷贝到新数组中时,用转移结点标记原数组的槽点位置,就是不会有线程对当前槽点做数据添加,会等待拷贝结束

5、数组的大小必须是2的n次方

  1. 如果不是2的n次方,会增加数组下标的碰撞几率,(2^n-1)&哈希值
  2. 2^n是全一,与哈希值做与运算,会保留哈希值的低位,只要低位不一样,碰撞就不会发生

6、为什么不直接使用hascode,而是使用h^(h>>>>16)&HASH_BITS

  1. 右移16位,整数型是32位,就是让高位和低位同时参与运算,避免了哈希值不同,但是低16位相同,导致哈希碰撞问题的出现,
  2. 和HASH_BITS做与运算(0111111…),是为了消除符号位,让哈希值都为正数

7、扩容的思路

  1. 创建大小为原来两倍的数组容器
  2. 吧数据从数组的队尾开始拷贝到新数组中去,吧原数组操作设置为转移结点
  3. 如果有新数据刚好添加时,就会等待,拷贝过程中对应位置上的数据不会发生变化的

8、hashTable和hashMap的异同

  1. hashmap是线程不安全的,hashtable是线程安全的,基本都被sych修饰
  2. 因为线程安全,一次只有一个线程去执行,所以效率比较低
  3. 初始化容量:hashtable初始化容量是11,而hashmap的初始化容量是16
  4. 扩容大小:hashtable扩容为原来的2n+1倍,而hashmap是原来的两倍
  5. 底层数据结构:map是链表+红黑,table没有
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值