HashMap面试高频题
HashMap和HashTabel的区别
相同点
- 两者都是基于hash表实现的,同样每个元素是一个k-v对,内部也是通过链表解决冲突问题,容量不足同样也会自动扩容
- 同样实现了Serializable接口,支持序列化,实现了Cloneable接口,能被克隆
不同点
- 继承的父类不同
- HashMap继承的自AbstractMap类型,但二者都实现Map接口.
- Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类了,子类自然很少生使用了
- HashMap属于线程不安全的,HashTable线程安全
- 解决办法就是使用ConcurrentHashMap
- HashMap的迭代器(Iterator)是fail-fast迭代器,故当有其他线程改变了HashMap的结构,(增加或者移除元素)抛出ConcurrentModificationException异常,而Hashtable的enumerator迭代器不是fail-fast的。但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别
- 包含的contains方法不同
- HashMap是没有Contains方法的,而包括,而包括ContainsValue和containsKey方法,而Hashtable则保留了该方法,包含了这些个方法
- 是否允许null值
- HashMap是允许有k-v值为null值,用containsValue和containsKey方法判断是否包含对应的键值对;Hash键值对都不能为空
- hash计算方式不同
- 为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。
- HashMap有和hash方法重新计算的key的hash值,因为hash冲突高,所以通过一种方法重算hash值方法;不过也是先要使用hashCode的方法计算出一个key的hash值,再将hash值与右移16位后相异或,从而得到新的hash值,减少hash碰撞,减少equals方法的使用,提高效率
铺垫位运算
hashMap中使用位运算来解决乘除的运算,能够更快的计算出 2^n次幂
key存储
- 底层key的实现是数组加链表,外加红黑树的数据结构,到达一定
- 当数组中出现hash冲突时将另一个值放入到链表中,由于链表边长后查询效率非常的低,
- 所以当链表长度达到一定的阈值后就会转化为红黑树
- 好的hash函数不同而值尽量生成不同的hash值
map.put()底层实现
- hash值的生成t=Integer类型
- int类型的hash值就是本身
- float类型的把小点去掉就是他的hash值
- Long类型的hash值就是, 右移后得到新的值和原先的值进行异或,得到就是新的hash值,每一个值都参与到hash计算中来
- String字符串的hash值计 ,避免出现乘法的出现,使用位运算,让每一个字符都参数hash运算
重写hashcode和equals时,
- 原则是hash值使用哪些值进行hash值的运算那么equals也得使用哪些只进行运算,引用数据类型的重写的方式,参照String类型的重写方式即可
put()方法的调用讲解
-
传进来的可以先计算出hash值针对key
-
在重写hashcode和equals的基础上再对hash值再进行扰动计算,右移动16位位运算
-
一开始new map的时候并未开始初始化长度,
-
计算出初始化的容量,第一次初始化逻辑1<<<4 =16,创建出一个数组将数据放入一个他table中
-
数组的长度为16,拓容阈值为12 其实是数组长度乘以0.75得到的,
-
当然hashMap允许我们传入一个初始化容量值,这个值如果传进去的不是2的幂次数时会自动tableset的方法会往上去找到一个2的幂次数,设置为初始化的容量值
-
首先需要先声明一下这里比较的都是key,key可以是对象引用数据类型,也可以是基本数据类型
- 根据计算出来的索引值,找到数组上的位置是否为空,
- 如果为空:那么直接放上去就行,
- 如果不为空:就需要先使用==比较key的地址值一不一样,
- **如果一样:**新的值将旧的值直接覆盖,将旧的value返回
- **如果不一样:**再使用equals进行比较了如果一样了就覆盖,将旧的value返回,他们是不是同一个对象,如果是那么就要进行值的覆盖,如果不是就进行尾插法进行添加
- 尾插法
- 链表长度大于八,会变成红黑树的逻辑,
- 但是会判断一下,如果数组长度总长度未大于64,只会进行拓容不会变为红黑树
- 在转为红黑树前会将单向的链表转化为双向的链表,方便后续的遍历(拓容后的迁移)
- 拓容逻辑又是,因为hash值是根据数组长度来进行hash值的计算的,长度变了那么计算的出来的hash值也会变化,但是由于底层设定数组的长度只能是2次幂,这个巧妙的设计呢就会出现原先是一个链表的,拓容后分为两个链表,这两个链表,低位和高位链表,的hash槽位置:如果是 j 另一个就是j+16 (16是旧数组长度值,这个值不固定,那么j当然也是通过旧的数组长度计算出来的),这些都是key hash值和数组长度-1计算出来的,具体细节需要多看源码
- 链表长度大于八,会变成红黑树的逻辑,
8. new的时候未触发数组,第一次时put进去时才会触发数组长度值
- 根据计算出来的索引值,找到数组上的位置是否为空,
为什么是不直接将key作为hash值的而是与高位的十六位做异或运算
- 不过也是先要使用hashCode的方法计算出一个key的hash值,再将hash值与右移16位后的hash值相异或,从而得到新的hash值,减少hash碰撞,减少equals方法的使用,提高效率
- 就是要进行扰动计算,就是编码者为使用者在进行一次hash值的计算,减少hash冲突
为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?
- 源码中为了避免乘除及求模计算,使用位运算来进行代替求模来进行哈希值的计算,而位运算则是二进制的位运算这样就必须的2次幂
- 首先由上面的得知如果传入的数组长度不是2的幂次方那么hash&len-1得到的hash值就不会是值的取模值,这样有可能会出现大量的hash冲突
- 这时就需要规范hash值的生成,数组长度必须得是2的幂次方
- 当然要是设置进去的不是二的幂次方也不要紧有Tableset的方法
- 然而new 一个map的时候是可以设定map的容量值的,但是,值设定有会是不固定的,有可能不是2的次幂那么就需要方法去计算,输入进去的初始化容量值去匹配最接近2的幂次的值,比如输入3 匹配4
- 输入7 匹配8 输入11匹配16 需要调用这样的一个方法,
为何重写hashcode一定要要重写equals
- 重写Hashcode,如果两个对象的额hash值不相等那么他们一定是不相等的,如果hash值就能进行值的判断,那么就不需要去调用equals方式进行比较那么比较速度相对较快,因为hash值是一开始就计算好的,重写hashcode更多体现的额是hash的复用提高比较效率,而不是减少equals的使用这么一说,因为如果hash冲突过多,equals调用的次数也相对较多,所以呢重写只是为了充分利用hash值,hash值不相等根本就不会去再调用equals方法了
谈一下hashMap中什么时候需要进行拓容,拓容resize()又是如何实现的?
-
当我们加入的元素大于拓容阈值时触发拓容,(当数组的长度大于当前容量*0.75时会进行一个扩容,数组长度)
-
需要注意的是不是数组长度的为16,有16个位置,并不是将12个位置都是用完毕才进行托容的例如下面的情况也会进行拓容
-
0.75这个负载因子计算出来的值代表的是一个拥挤程度,大于这个程度就应该拓容了
-
没有大于拓容阈值,链表长度大于八,数组长度小于64也会拓容
-
拓容后进行元素的迁移,迁移到新的数组上时
- 数组上只有一个值,直接使用 newTab[e.hash&(newCap-1)] = e,通俗就是重新计算hash槽位然后直接放进去就行
- 数组上是个链表
-
分低位链表和高位链表,低位就是原来的为位置,高位就是i+16的位置
-
然后根据计算将原先链表的数据分别归类于高低位的链表,形成两个链表,需要注意的是,最后两个表位的数据还存在着关系,最后表位数据指向一个null
-
红黑树的迁移则是将红黑树分割成两段的双向链表,如果说分割后这两个双向链表的长度小于6那么就会退化为单项链表,也就不会生成这个红黑树了,当然如果长度满足就会进行树化,
- 细节优化:当然也可能高位或者低位是没有元素,说明就算迁移后元素组成也没有变化,那样就不需要再树化了直接把头迁移就行
谈一下当两个对象的hashCode相等时会怎么样?
- 之后会进行两个可以的equals对比,如果可以相同key,相同直接value值会被覆盖,不同直接往链表上添加或者红黑树上
什么时候链表会变成红黑树呢
当加入第九个元素的是否才会由链表
并且数组长度大于64变为,先将整个链表转化为双向链表,目的是方便遍历和操作,然后再会进行红黑树的转化
什么时候链表会变成红黑树呢
当加入第九个元素的是否才会由链表
并且数组长度大于64变为,先将整个链表转化为双向链表,目的是方便遍历和操作,然后再会进行红黑树的转化