hashmap解析与1.7和1.8 put方法流程图与常见问题

(本文只是基本普及,可能存在一定程度的问题,如果有问题或者错误还请指出)

哈希

顾名思义,通过一定的哈希算法进行映射,将数据映射到不同的位置。数据项一般为键值对。

哈希算法:

  • 直接定址法:对键值进行线性变换得到哈希地址

  • 数字分析法:找到数据中区别较大的部分,组成哈希地址

  • 平方取中法:取键值平方后的中间几位作为哈希地址

  • 折叠法:将键值分为几部分,将这几部分进行叠加得到哈希地址

  • 除留取余法:键对于哈希表长进行取余,得到的结果作为哈希地址

    hashmap用的方法我个人认为是将折叠法和除留取余法进行结合。由于键值会有hashcode,然后取hashcode高低位进行异或操作,让其得到的hash值更加平均,再对于哈希表长进行相当于取余操作的 【n-1 & hash】得到哈希表长范围内的哈希地址。

哈希冲突处理方法:

  • 开放定址法:
    如果冲突,在原来结果的基础上,进行+1,+2等的操作,找不冲突的位置。
    缺点:不能随意删除节点,会影响其他节点的查找,只能标记为已删除节点。查找时,由于冲突的左右移动解决方法,也会影响查找速度
  • 链地址法:
    将冲突部分链接成一个链表。
    简单。
    但是当链表长度过长会一定程度影响查找速度。
  • 再哈希法
    冲突时,再用另一个哈希算法对于键进行哈希
    速度较慢。而且如果再冲突还需再哈希。
  • 公共溢出区
    如果溢出区有冲突,也得耗费一定时间。

put方法流程图

自制hashmap1.7 put方法流程图
hashmap解析与1.7和1.8 put方法流程图与常见问题_第1张图片
自制hashmap1.8 put方法流程图
hashmap解析与1.7和1.8 put方法流程图与常见问题_第2张图片

常见问题点(持续更新)

答案都是个人总结的,可能存在不足,请辩证看待。

  1. 为什么hashmap要选择2的幂次方作为哈希表的长度?
    答:因为这样可以提高查找效率,减少冲突。由于hashmap采用hashcode经过函数扰动计算出hash值,hash值是整数,那么就是-21亿-21亿之间的数,那么40亿左右的数,可以保证冲突发生的几率很小。但是不可能每次哈希表长都设置的很大,那么此处根据基本的哈希算法,选用除留取余法是比较合适的。但是这块出于速度与转移元素的考量,选择了2的幂次方作为哈希表长度,这样,哈希表长度减一后的值低位全部都是1,有更好的散列性,而且比直接取余操作快。
    其次,在进行元素转移时,保证扩容后,新老索引的一致性,大大减少了已经散列良好的老数组的数据位置重新调换。
    (元素转移计算索引时,要么是原来的位置,要么是原来的位置+扩容前数组的长度的位置)
  2. hashmap平均时间复杂度为多少?为什么?
    答:时间复杂度平均为O(1)。因为哈希表数据结构本身就是为了实现常数级别的根据键查找的功能。由于hashmap良好的扩容加上超过8元素就会扩容成红黑树的特性,大多数查找都会在常数时间内完成。而且根据hashmap的官方注释,经过测试,60%的链表长度都为0,即一次即可找到,30%链表长度为1,只需要两次寻找,其余加起来占10%,链表长度超过8的可能性更是很小,0.00000006,几乎很难出现红黑树。
    所以大多数时间集中在一次查找和两次查找,平均时间就是O(1)。
  3. hashmap的扩容机制
    答:hashmap1.7扩容会在当前数据元素大于等于加载因子乘哈希表长度,也就是大于等于阈值会进行扩容。扩容时,为了保证哈希表的长度仍未2的幂次方,新的哈希表长度扩容为原来的2倍。再遍历数组,进行内容转移。转移时由于转移链表采用的头插法,导致在新的哈希表中链表倒序,可能会出现循环链表。
    hashmap1.8扩容会在当前数据元素个数大于阈值时进行扩容,或者当有链表长度大于等于8要变换为红黑树但哈希表长度小于64时进行扩容,扩容也是将新的哈希表长度设置为原来的2倍,再进行转移。但是hashmap1.8采用的尾插法转义链表,所以一般不会出现循环链表。
  4. 除了拉链法还有别的解决冲突方法吗?
    答:
    1. 开放定址法:出现冲突后,查看冲突位置的附近是否有数据,没有则放到该位置。
    2. 再哈希法:出现冲突后,使用另一个哈希函数进行哈希,再找一个哈希表的位置。
    3. 公共溢出区:出现冲突后,将冲突的数据放到溢出区。
  5. hashmap是线程安全的吗?请解释
    答:1.7和1.8都不是线程安全的。
    1.7中,在resize时,转移元素的transfer方法,由于采用的头插法和直接覆盖新数组位置,在多线程情况下,有可能造成循环链表,形成死循环。
    1.8中,通过尾插法和先链接到指定节点上,最后再覆盖新数组的情况,不太可能出现循环链表但是也有别的问题。
    1.7和1.8共有的问题是,多线程情况下,线程的操作可能被覆盖或者被忽视,导致数据丢失。其余也有例如size只是采用transient修饰,并没有采用violet修饰,所以size也可能产生问题。
    (补充:如果此时被问到,是否可以通过加volatile关键字保证size可以正常,是不行的,由于每次put完毕或者删除,都会对size进行自增或者自减操作,而自增或者自减操作是非原子性的,所以不行。volatile关键字只能保证被修饰的变量可以保证多线程对这个变量的可见性.i++操作,变量的写操作依赖当前值,所以不能保证线程安全)
  6. hashmap的遍历方式
    1. 使用迭代器迭代entryset,可以调用迭代器的remove方法删除元素,是安全的
    2. 使用迭代器迭代keyset,再get(key),无法调用iterator的remove删除元素,不安全。
    3. 使用for循环迭代entryset,不能随意删除元素,不安全
    4. 使用for循环迭代keyset,不能随意删除元素,不安全
    5. 使用lambda的forEach迭代,可以先removeif,然后再删除,是安全的,直接删除不安全
    6. 使用streamAPI迭代,可以先filter,再删除,是安全的,直接删除不安全。
      总结:不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然也可以在 for 循环前删除数据在遍历也是线程安全的。(来自hashmap的7种遍历方式)
  7. 介绍一下hashmap 。
    答:hashmap是哈希表的一种实现,数据采用键值对的方式。在Java的hashmap中,哈希算法采用的是除留取余法的变种,通过hash值和数组长度减一进行与操作实现hash值范围落在哈希表长度内的方法。解决冲突采用的是链地址法,当有数据发生冲突时,将冲突的部分链接成链表。
    hashmap通过key的hashcode经过扰动函数hash()处理后得到hash值,通过(n-1) & hash得到元素存放在哈希表中的位置,如果当前位置存在元素,判断当前元素和存在的元素的hash值和key值是否相同,相同会直接覆盖并且返回旧元素,否则通过拉链法解决冲突。
    hashmap1.7以及之前哈希表的实现是数组加链表的形式,1.8之后哈希表的实现改为了数组加链表,且当链表长度默认大于8并且数组长度小于64时会将链表转为红黑树,当链表长度小于等于7时,又会将红黑树转移为链表。
    1.8扩容时是大于阈值会进行扩容,或者当有链表长度大于8且数组长度小于64时扩容。
  8. equals方法和hashcode方法。
    答:equals用来判断两个对象是否相同,一般未重写时,对于值类型,判断值是否相等,对于引用类型,会判断是否引用的是同一个堆上的内容(即地址值是否相同)。
    由于默认规定,equals方法被覆盖过,那么hashcode方法也必须被覆盖。因为默认规定,两个对象相同,那么hashcode必须相同。因为默认情况下,hashcode是堆上对象的独特值,所以可能两个对象内容相同,但是hashcode可能不同,所以一般重写equals方法就需要重写hashcode方法。
    hashcode方法主要用于在散列时判断两个对象是否可能相同以及去重时判断两个对象是否可能相同。因为哈希可能会冲突,所以只能保证两个相同对象的hashcode相同,不能保证hashcode相同下两个对象相同。
  9. ==equals的区别
    答:
    • 对于基本类型来说,==比较的是 值是否相同。
    • 对于引用数据类型来说,==比较的是两个对象是否指向同一个对象地址。
    • 对于引用类型,equals如果没有被重写,那么比较它们的地址是否相等;如果equals方法被重写,则比较的是地址里的内容。
  10. 为什么hashmap判断元素相同时还要判断hashcode?
    答:由于键是object,判断两个object是否相等,一般采用的会是equals方法。但是hashcode可以用来判断两个object是否不相同。如果hashcode不相同,那么就不用判断equals方法了,省时间。

你可能感兴趣的:(Java集合解析)