谈谈你对HashMap的理解

这个看似简单,我们只要说出对hashmap的理解,自由发挥就好,但是好的回答自然会让面试官刮目相看。围绕这个点要是答好了会引申出超级多的知识点

在这里个人简单总结下:

回答顺序

通过下面的回答顺序,对于初级开发来说妥妥的够了!

Map的接口结构

将集合的框架体系说出来吧!顺便把collection的框架体系也说出来吧!

双列集合:

image-20211218202827550

单列集合

image-20211218202909441

HashMap的put

顺便说下HashSet的底层就是HashMap!

源码如下

image-20211218203438701

HashSet和HashMap的区别是:HashSet将value值设为空对象(空的Object)

image-20211218203851581 image-20211218203856564

put过程如下:

  1. HashMap底层维护了Node数组的table。(ransient Node<K,V>[] table)
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75
  3. 当添加 k - v 值时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素
  4. 如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,直接替换val;如果不相等直接添加(1.7头插,1.8尾插),而且需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
  5. 当我们初始化HashMap后,HashMap的长度为0,当首次put元素的时候才将HashMap的长度初始化为16。 然后阈值是16 * 0.75 = 12,也就是当节点数量超过12就进行扩容,扩容的倍数为2(length长度:16 -> 32 ;threshould阈值:12 -> 24)
  6. version >=Java8,如果一条链表的元素个数超过TREEIFY THRESHOLD(默认是8),并且
    table的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)

注:hashmap的容量是在第一次put的时候才初始化其容量的,而不是new hashmap的时候指定!

hashmap的hash算法

JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

image-20220108220852294

img

为什么要用异或运算符?

举个栗子:

  • 如果用与&运算, 1 & 0 = 0; 1& 1 = 1; 0 & 1 = 0; 0 & 0 = 0; (与运算如果都为1才为1)
  • 如果用或|运算, 1 | 0 = 1; 1| 1 = 1; 0 | 1 = 1; 0 | 0 = 0; (或运算只要有一个是1就是1)
  • 如果用异或^运算,1 ^ 0 = 1; 1^ 1 = 0; 0 ^ 1 = 1; 0 ^ 0 = 0; (异或运算如果两个位不相同就为1)

可见使用异或运算保证了0和1的结果均匀分布,而且保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。

扩容为什么要扩容2倍

image-20220216201257430

image-20220109144444004

查看源码,发现源码中无论是在桶中第一次存放数据,还是在在resize()的时候将元素重新分配,都是使用hash & (n - 1)这个算法来确定元素在桶的位置 (hash与上长度-1)

当HashMap的容量是16时,它的二进制是10000,15的二进制是01111,与hash值得计算结果如下

image-20220109144734131

如果容量不是2的n次幂的情况,当容量为10时,二进制为01010,9的二进制是01001,向里面添加同样的元素,结果为

image-20220109144721209

可以看出,有三个不同的元素进过&运算得出了同样的结果,严重的hash碰撞了

为什么要重写hashcode和equals?

一般的,hashmap中的key建议使用String类型,如果想要使用自定义的对象用作hashmap的key,就要重写hashcode和equals方法

重写hashcode和equals对HashMap的重要性

在hashmap中是通过hashcode决定元素放入哪个索引(桶)中,然后通过equals判断和索引中对应链表中的每个元素是否相同,如果有相同 ->替换,如果不相同 -> 直接放 ( jdk version >8尾插法、<8头插法)

hashcode和equals的原理解读

如果不重写hashcode方法,hash值就是内存分配的地址值;重写hashcode方法,hash值就是根据对象中的成员变量值经过一系列的算法求得的。

如果不重写equals方法,调用equals方法默认走地址值;重写equals方法后调用equals是通过变量值进行比较。

不重写hashcode和equals对HashMap的影响

如果在hashmap中如果只重写了hashcode没重写equals:如果我们添加两个相同内容的User对象放入hashmap,添加完第一个User后,添加第二个User时,hash值和第一个User相同,会走equals到同一个索引中找是否存在相同的元素,此时我们因为没重写equals,比较的是地址值(两个虽然内容相同的对象地址值是不同的),因此第二个相同的User也插入到了同一个索引中。

如果在hashmap中如果只重写了equals没重写hashcode:
这个最好理解,上面说到:两个虽然内容相同的对象地址值是不同的,因为没重写hashcode,hash值是地址值 ===> 两个相同内容的对象地址值不同,hash值也不同 ===> 他们两个被放到了hashmap不同的索引中。

因此,hashcode和equals必须重写,两个缺一不可,如果缺一个,就会在Hashmap中添加出相同的key对象,通过这个key对象会查出多个数据可能会导致系统的bug。同时还会导致HashSet失去了去重功能

数据结构

version < JDK 1.8数组 + 链表

version >= JDK 1.8 数组 + 链表 + 红黑树

**链表转化成红黑树:**在JDK 1.8 之后红黑树是在索引中链表长度>8,且索引数量 > 64 才开始树化,如果链表长度 > 8但是索引数量 < 64,暂时不树化,而是在树化的方法中treeifyBin中进行resize扩容

红黑树转化成链表:

在对应索引的节点数小于6时,从该索引中的红黑树结构转化成链表

为什么链表转化成红黑树阈值为8,红黑树转化成链表阈值为6?

原因就是二者之间的差值可以防止链表和树之间的频繁转换!

如果红黑树转化成链表阈值也设为8,如果索引中对应元素个数大于8就从链表转换成红黑树,小于8则从树结构转换成链表。如果HashMap不停的插入,删除元素,链表个数在8左右徘徊,就会频繁的发生红黑树转链表,链表转红黑树。避免数据结构频繁转换造成性能的浪费!

HashMap为什么使用红黑树

如果用二叉查找树,由于它的不平衡特性,极端情况下的时间复杂度会升级为O(n)

红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最好和最坏情况下均为O(log n)。加快检索速率。

JDK 1.8 之前没有红黑树,为了提高效率采用红黑树,避免黑客使用哈希碰撞攻击hashmap,使得索引中的链表过长,增大查询时间复杂度,消耗系统性能。

数据结构

version < JDK 1.8数组 + 链表

version >= JDK 1.8 数组 + 链表 + 红黑树

注:

JDK 1.8 之前没有红黑树,为了提高效率采用红黑树,避免黑客使用哈希碰撞攻击hashmap,使得索引中的链表过长,增大查询时间复杂度,消耗系统性能。

**链表转化成红黑树:**在JDK 1.8 之后红黑树是在索引中链表长度>8,且索引数量 > 64 才开始树化,如果链表长度 > 8但是索引数量 < 64,暂时不树化,而是在树化的方法中treeifyBin中进行resize扩容

红黑树转化成链表:

在对应索引的节点数小于6时,从该索引中的红黑树结构转化成链表

为什么链表转化成红黑树阈值为8,红黑树转化成链表阈值为6?

原因就是二者之间的差值可以防止链表和树之间的频繁转换!

如果红黑树转化成链表阈值也设为8,如果索引中对应元素个数大于8就从链表转换成红黑树,小于8则从树结构转换成链表。如果HashMap不停的插入,删除元素,链表个数在8左右徘徊,就会频繁的发生红黑树转链表,链表转红黑树。避免数据结构频繁转换造成性能的浪费!

线程安全

Hashmap是线程不安全的,不支持并发写,线程安全的map有HashTable、SynchronizedMap、ConcurrentHashMap

性能上HashTable <= SynchronizedMap < ConcurrentHashMap

现在扯到了ConcurrentHashMap,那就和面试官聊聊ConcurrentHashMap吧! 这不就完美了!

ConcurrentHashMap

ConcurrentHashMap 底层结构是数组 + 链表 + 红黑树

JDK1.7和JDK1.8中的ConcurrentHashmap对比

  • JDK1.7中的ConcurrentHashMap

    内部主要是一个Segment数组,而数组的每一项又是一个HashEntry数组,元素都存在HashEntry数组里。因为每次锁定的是Segment对象,也就是整个HashEntry数组,所以又叫分段锁。

    而且在jdk1.7中最多只能有16个segment,也就是说最大支持16个并发

  • JDK1.8中的ConcurrentHashMap

    舍弃了分段锁的实现方式,元素都存在Node数组中,每次锁住的是一个Node对象,而不是某一段数组。

    对比JDK1.7,ConcurrentHashMap在jdk1.8中的优化有哪些:

    1. 锁的粒度更细:JDK1.7中以segment作为锁,而segment下面还有HashEntry数组,JDK1.8中以Node作为锁,所以支持的写的并发度更高
    2. JDK1.8的链表中引用了红黑树

image-20211007002730165

put操作

ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:

  • 根据 key 计算出 hashcode 。

  • 判断是否需要进行初始化。即为当前 key 定位出的 Node,如果当前node中的数据为空则利用 CAS 尝试写入,失败则自旋保证成功。

  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

  • 如果都不满足,则利用 synchronized 锁写入数据,锁住的是整个node。

  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

    总的来说就是当一个node中放入第一个数据的时候使用CAS自旋操作,后续的数据放入都是使用细粒度的synhronized锁

ConcurrentHashMap对比HashTable和Synchornizedmap的优势

同样是线程安全的Map,ConcurrentHashMap比HashTable效率要高。

HashTable和Synchornizedmap在写入的时候是锁住整个Node[ ],读的时候也是加锁的。

而ConcurrentHashMap写的时候采用自旋锁 + 分段锁(第一次添加的时候是自旋锁,后序添加锁定的是一个Node),写的时候锁的粒度更小。ConcurrentHashMap效率提高主要的地方是在读上,读的时候完全不加锁。

image-20210904105656108 image-20210904104507675

get操作

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。
image-20210904161753204

**注:**TreeMap用的是红黑树,查找的时候效率高

没有ConcurrenTreeMap的原因是CAS在如果用在树形结构上会太复杂

因此出了个基于跳表结构实现的并发map:ConcurrentSkipListMap 跳表实现。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: HashMapJava 中一种非常常用的数据结构,它是基于哈希表实现的一种键值对映射容器。 在 HashMap 中,每个元素都有一个唯一的键和一个对应的值。通过哈希函数对键进行散列,可以快速地在哈希表中定位到对应的值,因此 HashMap 具有非常高效的查找速度。 当往 HashMap 中添加元素时,它会根据键的哈希值计算出对应的桶(bucket)的位置,然后将键值对存储在桶中。如果多个键的哈希值相同,那么它们会被存储在同一个桶中,这时候需要通过链表或红黑树等数据结构来存储这些键值对,以避免哈希冲突。 HashMap 的常用操作包括添加元素、获取元素、删除元素等,其时间复杂度均为 O(1)。不过,当哈希表中的元素数量达到一定阈值时,为了避免哈希冲突过多而导致性能下降,HashMap 会自动进行扩容操作。 需要注意的是,由于哈希表是无序的,因此 HashMap 中的元素是没有顺序的。如果需要有序的键值对映射容器,可以考虑使用 TreeMap 等其他数据结构。 ### 回答2: HashMapJava中常用的数据结构之一,它是一个键值对存储的集合。它的实现原理是基于哈希表,使用键的哈希码进行索引,能够快速地根据键找到对应的值。以下是我对HashMap理解。 首先,HashMap允许使用null作为键和值,但是建议尽量避免使用null键,因为它在哈希表中的索引位置不确定,可能会导致性能下降。 其次,HashMap的put和get操作的时间复杂度都是O(1),即常数时间复杂度。这是因为HashMap内部使用一个数组来存储元素,通过计算键的哈希码,将其映射到数组的索引位置,从而可以直接访问到对应的值,而不需要遍历整个集合。 另外,当HashMap的负载因子超过设定的阈值时,会触发扩容操作。扩容会重新计算键的哈希码,并重新分配数组,以提高HashMap的效率和容量。 需要注意的是,HashMap并不是线程安全的,如果多个线程同时对HashMap进行修改,可能会导致数据不一致。如果需要在多线程环境中使用HashMap,可以使用ConcurrentHashMap或者手动进行同步操作来保证线程安全。 此外,HashMap的遍历是无序的,因为它是根据键的哈希码来存储和访问数据的。如果需要有序的遍历,可以使用LinkedHashMap,它保持元素的插入顺序或访问顺序。 最后,使用HashMap时需要注意键的hashCode和equals方法的正确实现,以确保键的唯一性和正确的存取。 总的来HashMap是一个高效的数据结构,能够快速地根据键找到对应的值。我们可以利用它来实现缓存、查找等常见的功能。但是在使用过程中需要注意线程安全和哈希码等细节的处理,以避免潜在的问题。 ### 回答3: HashMapJava中的一种数据结构,它实现了Map接口。它是基于哈希表的,使用键值对的方式存储数据。我对HashMap理解主要有以下几点: 首先,HashMap使用哈希函数将存放的键映射到存储桶的索引上。这样可以通过键快速定位到存储的值,提高了数据的访问效率。不同的键可能会映射到相同的索引,这就是哈希碰撞。HashMap通过链表或红黑树的形式解决了哈希碰撞的问题,确保了高效的查找和插入操作。 其次,HashMap允许存放null值和null键。它使用equals()方法判断两个键是否相等,使用hashCode()方法计算键的哈希码。为了提高效率,好的HashMap应该具有良好的散列分布,即尽量避免哈希碰撞,使得键尽可能均匀地分布在各个存储桶中。 此外,HashMap是非线程安全的,不适用于多线程环境。如果需要在多线程环境下使用,可以考虑使用ConcurrentHashMap,它提供了线程安全的操作。 最后,HashMap的容量会根据实际存储的键值对数量动态扩容和收缩。当HashMap的大小超过负载因子与当前容量的乘积时,会自动扩容。扩容后,原有的键值对需要重新计算哈希码和存放到新的存储桶中,这会增加一定的开销。因此,在使用HashMap时,需要合理设置负载因子,避免频繁的扩容操作。 总之,HashMap是一种高效的数据结构,提供了快速的查找和插入操作。但是,需要注意其不是线程安全的,而且在使用时需要注意负载因子和散列分布的优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值