HashMap

HashMap

HashMap基础问题

HashMap内部节点是有序的吗?

  • 是无序的,根据hash值随机插入

重写equals方法需同时重写hashCode方法?

  • 当用其中的一个作为键保存到HashMap、HashTable或 HashSet中,再以“相等的”另一个作为键查找他们的时候,则根本找不到。

  • 为了保证当两个对象通过equals()方法比较相等时,那么他们的hashCode值也一定要保证相等。

HashMap 什么时候开辟bucket数组占用内存?

  • 在HashMap第一次put的时候,无论Java 8还是Java 7都是这样实现的。

  • 两个版本的实现中,桶数组的大小都是2的正整数幂。

HashMap如何处理key为null的键值对?

  • HashMap允许put key为null的键值对

  • 这样的键值对都放到了桶数组的第 0个桶中。

为什么加载因子是0.75?

扩容因子表示哈希表中元素的填充程度

  • 值越大,元素越多,虽然空间利用率较高,但是哈希冲突概率也会增加;

  • 值越小,元素越少,虽然哈希冲突概率降低,但是内存空间浪费较多,增加扩容的频率。

这个值是根据空间利用率和冲突概率之间的一个平衡,通过泊松分布算法得到的一个折中的值。

HashMap默认值都是什么,什么时候扩容?

  • 若没有自定义容量和因子,则默认---->HashMap的容量默认16 ,扩容因子0.75

threshold 计算公式:capacity * loadFactor

即使指定的大小不是2的整数次幂,那么 Hash 会选择大于该数字的第一个 2 的幂作为容量(3->4、7->8、9->16)。

  • 扩容:当容器中的HashMap总数>=threshold 时,创建一个新的Entry空数组,长度是原数组的2倍。 遍历原Entry数组,把所有的Entry重新Hash到新数组。长度扩大以后,Hash的位置也随之改变。

  • Hash的位置也随之改变:

由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1

扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001
1111。

因为是& 运算,1和任何数 &
都是它本身,那就分二种情况,原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)

关于设置 HashMap 的初始化容量大小

  • (需要存储的元素个数/0.75(默认))+1

Jdk 并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个 2 的幂。


HashMap进阶问题

为何HashMap的数组长度一定是2的次幂?

  1. 可以通过(length - 1) & key.hash()这样的位运算快速寻址

  2. 当桶数组长度为2的正整数幂时,扩容迁移的时候不需要再重新通过哈希定位新的位置了。

同一个桶中的元素均匀的散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一半留在原先的桶中,一半放到了新的桶中。

:: 如果在即将扩容的那个位上key.hash()的二进制值为0,则扩容后在桶中的地址不变,否则,扩容后的最高位变为了1,新的地址也可以快速计算出来newIndex = oldCap + oldIndex;

java8之后为啥改为尾部插入呢?

  • 1.7是头插法,在并发场景下导致链表成环的问题。而在1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

JDK8相比于JDK7有优化吗?为什么要做这几点优化?

  1. hash()做了优化,1.7做了4次移位4次异或,1.8改成了1次(多了用处也不大)
  2. 数组+链表改成了数组+链表+红黑树;
  3. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的 后继节点,1.8遍历链表,将元素放置到链表的最后;
  4. 扩容的时候1.7需要对原数组中的元素进行重新 hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  5. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
  • 防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);

  • 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环


HashMap的工作原理(存,取)

A.存储对象

将K/V键值传给put()方法:

①、计算K的哈希值—hashcode(),调用hash()扰乱,indexFor()进一步处理来获取实际的存储位置,得数组下标;在这里插入图片描述

②、如果发现当前的桶数组为null,则调用resize()方法进行初始化

  • 如果K的hash值在HashMap中不存在,则执行插入,若存在,则发生哈希碰撞,采用了链地址法处理哈希碰撞;
  • 如果K的hash值在HashMap中存在,链表中数据一一比对,若有true,则更新键值对;
  • 如果K的hash值在HashMap中存在,链表中数据一一比对,都是false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式),注意如果链表超度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构。

③、put完后调整数组大小,如果HashMap的总数超过threshold就要resize。


B.获取对象

将K传给get()方法:

  • ①、计算K的hash值,从而获取该键值所在数组下标;
  • ②、遍历该下标下的链表or红黑树,equals()方法查找。

hashCode是定位的,定位存储位置;

equals是定性的,比较两者是否相等。


如何解决Hash冲突

解决hash冲突的方法有四个:链地址法、开放地址法、再哈希法和建立公共溢出区法

1.链地址法

(HashMap即是采用了链地址法

就是数组链表红黑树里的链表

2.开放地址法

发生冲突,继续寻找下一块未被占用的存储地址

3.再哈希法

同时构造多个不同的哈希函数,当哈希地址发生冲突时,再用下一个哈希函数计算……,直到冲突不再产生。

4.建立公共溢出区法

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。


有关hash()的问题

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

  • 得到的hashCode转化为32位二进制,低16bit和高16bit做了一个异或

  • 由于计算出来的哈希值都要与length - 1做&运算得到数组下标,那就意味着计算出来的hash值只有低位有效,这样会加大碰撞几率,因此让高16位与低16位做异或,让低位保留部分高位信息,且异或保证了对象的hashCode的32位值只要有一位发生改变,整个hash()返回值就会改变,减少哈希碰撞。

  • 还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。

hash函数能不能直接用key的hashcode?

因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。

int值范围为-2147483648~2147483647, 前后加起来大概40亿的映射空间。

只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
  • 但问题是一个40亿长度的数组,内存是放不下的

什么才是一个好的hash函数呢?

  1. 计算出来的哈希值足够散列,能够有效减少哈希碰撞

  2. 本身能够快速计算得出,因为HashMap每次调用get和put的时候都会调用hash方法


红黑树有关问题

桶中的元素链表何时转换为红黑树,什么时候转回链表,为什么要这么设计?

  • 当同一个桶中的元素数量大于等于8的时候元素中的链表转换为红黑树,变为红黑树的目的是为了高效的查询

  • 反之,当桶中的元素数量小于等于6的时候又会转为链表,这样做的原因是避免红黑树和链表之间频繁转换,引起性能损耗

Java 8中为什么要引进红黑树,是为了解决什么场景的问题?

  • 引入红黑树是为了避免hash性能急剧下降,引起HashMap的读写性能急剧下降的场景

  • 正常情况下,一般是不会用到红黑树的,在一些极端场景下,假如客户端实现了一个性能拙劣的 hashCode方法,可以保证HashMap的读写复杂度不会低于O(logn)


HashMap与其它集合

HashMap与HashTable的区别

  • HashTable:线程安全集合类,所有方法都加了Synchronized锁,性能要差于HashMap,底层是数组+链表,链表主要用来解决哈希冲突,初始容量默认11,不支持使用null作为key

  • HashMap:线程不安全的集合类,底层是数组+链表+红黑树(Jdk1.8新引入的),初始容量默认16,支持使用null作为key,会把它作为 0来存储

有什么线程安全的类代替么?

Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。

  • HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大,并发度很低,最多同时允许一个线程访问。

  • Collections.synchronizedMap是使用 Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现。

  • ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。

一般都会使用HashTable或者ConcurrentHashMap,但是因为前者的并发度的原因基本上没啥使用场景了,所以存在线程不安全的场景使用的是ConcurrentHashMap。

你知道ConcurrentHashMap的分段锁的实现原理吗?

  • ConcurrentHashMap的分段锁实现原理是通过将哈希表分成若干个Segment,并为每个Segment单独加锁来实现并发访问控制的。

  • Segment锁是可重入的,即同一个线程可以多次获取同一个Segment的锁而不会造成死锁。

  • 此外,ConcurrentHashMap还使用了CAS(Compare and Swap)算法来实现对Segment的修改操作,这种算法可以在不阻塞其他线程的情况下完成操作,从而提高了并发性能。

  • 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。 线程A锁住A节点所在链表,线程B锁住B节点所在链表,操作互不干涉。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

有没有有序的Map?

LinkedHashMap 和 TreeMap

LinkedHashMap怎么实现有序的?

  • LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值