Java面试之集合类(1)Hashmap

以下内容来自网络整理,侵删
说说List,Set,Map三者的区别?
  • List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
  • Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
  • Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
Put方法
  • 生成hash值,方法是根据key生成hashcode,然后将hashcode右移16位,再与原值异或运算,这样做可以把hashcode的高区与低区的二进制特征混合到低区,降低哈希碰撞概率,使得数据分布更平均
  • 用(n-1)&hash定位到数组位置。如果定位到的数组位置没有元素 就直接插入。
  • 如果定位到的数组位置有元素就和要插入的key比较。如果key相同就直接覆盖;如果key不相同,就判断table[i]是否是一个树节点,如果是就红黑树插入。如果不是就遍历链表插入(插入的是链表尾部)。然后判断链表长度是否大于8。如果大于8,则转为红黑树。
  • 判断是否需要扩容
Get方法
  • 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
  • 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
  • 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
  • 遍历链表,直到找到相等(==或equals)的 key
  • 注意:由于hashmap允许null值,所以当返回值为null时,无法判断元素是否存在。为了判断元素是否存在,应该使用containskey这个方法。
扩容机制
  • 默认装载因子0.75.这个值不可以修改。
  • 如果容器内元素个数超过(数组大小*装载因子),触发扩容。
  • 容器大小乘2。新建一个2倍大小的容器,原来的元素,重新放入一遍。
  • 非常耗时的操作,所以初始化时,预先分配合适大小的空间。
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
  • hashCode()方法返回的是int整数类型,有40亿个映射空间,设备上不可能提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置
  • 解决方案如下
    • 将hashcode两次扰动,获得hash值
    • 然后在保证数组长度为2的幂次方的前提下,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储
为什么数组长度要保证为2的幂次方呢?
  • 只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位
  • 如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快
  • 空间不浪费。如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大
那为什么是两次扰动呢?
  • HashMap初始的容量大小为16,要远小于int类型的范围,如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算
  • 这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突;两次就够了,已经达到了高位低位同时参与运算的目的
HashMap jdk1.7头插法插入为什么会造成死循环?

情景:有两个线程分别执行扩容操作

  • 线程A执行完Entry<K,V> next = e.next;线程A挂起;
  • 线程B完整的执行完整个扩容流程;
  • 线程A唤醒,继续之前的往下执行,当while循环执行3次后可能会形成环形链表。
  • 所以使用头插会改变链表节点原有的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了
  • 举例说明:
    • 假设旧表的初始长度为2,此时已经在下标为1的位置存放了两个元素,key依次是3和7,3指向7,再put第三个元素的时候考虑需要扩容;
      在这里插入图片描述
      • 此刻有两个线程A,B都进行put操作,线程A先扩容,执行到代码Entry<K,V> next = e.next;执行完这段代码,线程A挂起;线程B执行完流程后,新的table可能是7指向3
      • 线程A唤醒后,因为线程B的操作,key(7) 指向key(3),while循环中遍历的顺序是key(3)->key(7)->key(3)->null遍历,所以导致环形链表
HashMap jdk1.7为什么非线程安全?
  • 这个情况是比较多的
  • 比如两个线程A,B 插入的数据索引到同一个下标
    • 如果线程A此时已经获得链表最后一个结点,马上要尾插法插入时,线程A挂起;线程B完成流程,插入链表最后一个结点;线程A唤醒时,不知道链表最后一个结点已经更新,就直接插入,线程B插入的数据被覆盖。
    • 或者另外一个例子,线程A检查发现发现没有产生Hash碰撞,就要直接插入时,挂起;线程B插入;线程A唤醒时,因为已经检查过hash碰撞了,所以也直接插入,线程B插入的数据被覆盖。
为什么HashMap中String、Integer这样的包装类适合作为K?

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  • 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况
重写hashcode()和equals()方法
  • HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等 的。若equals()不相等则认为他们不相等。
  • 在集合中,即便有相同含义的两个对象,hashcode也是不相等的,因为比较的是其地址。同理,equals方法,默认也是比较地址,返回也是不相等。所以两个都要重写。
  • Java中也规定,如果equals相等,那么hashcode必须相等。确保元素的唯一性
  • 重写hashcode()和equals()方法的样例:
    https://blog.csdn.net/qq_40378034/article/details/100598000
HashMap在JDK1.7和JDK1.8中有哪些不同?

在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值