Hashmap以及为什么equals,hashcode要同时重写

4 篇文章 0 订阅

基于散列的集合

hashMap术语介绍:

桶: 就是hashmap的table数组

bin: 就是挂在数组上的链表

TreeNode: 红黑树

capacity: table总容量

MIN_TREEIFY_CAPACITY :64 转化为红黑树table最小大小

TREEIFY_THRESHOLD :8 转化为红黑树的阈值

loadFactor: 0.75 table扩容因子,当实际length大于等于 capacity*loadFactor时会进行扩容,并且扩容是按照2的整数次幂,原因 下 面解释

threshold: capacity*loadFactor

https://blog.csdn.net/qq_27409289/article/details/92759730?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

关于jdk1.8之后hashmap在链表长度为8时转为红黑树

https://blog.csdn.net/danxiaodeshitou/article/details/108175535

1.为什么用红黑树?

链表的时间复杂度是O(n),红黑树的时间复杂度O(logn) ,速度明显提升。

2.为什么不一开始用红黑树?

因为树节点所占空间约是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。

3.为什么链表长度到8才变为红黑树?

https://blog.csdn.net/qq_27409289/article/details/92759730?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。

注:实际上转换红黑树有个大前提,就是当前hash table的长度也就是HashMap的capacity(不是size)不能小于64.小于64就只是做个扩容.

关于HashMap扩容机制:**

应试: 由泊松分布得出加载因子在0.6~0.8范围内时空间利用率和查询效率最好 (0.75*2n刚好为整数)-》2n的原因时因为求存放位置算法用的是hashcode与length-1,而2n次方-1刚好二进制全为一,这样的情况下,存放在同一个位置概率变低,即碰撞可能性降低。

总结:*

HashMap默认加载因子为什么选择0.75?(阿里) - aspirant - 博客园 (cnblogs.com)

​ 提高空间利用率和减少查询成本的折中(空间利用率和查询效率的平衡)

​ 加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但是导致碰撞可能增加,也就增加了查询时间成本;

​ 加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了resize操作的次数。(这个操作十分消耗性能即资源) (加载因子低影响空间和性能资源)

**resize:**最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize

扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

1)结构:(可以说是散列表)

在这里插入图片描述

2)数组长度为什么是16(或者是2的幂次方):

hash算法:

  1. static int indexFor(int h, int length) {
  2. ​ return h & (length-1);
  3. }

为什么用与呢?
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,

位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

在这里插入图片描述

看上图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率

​ 数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

转至:https://www.cnblogs.com/williamjie/p/9358291.html

为什么重写equals后要重写hashcode

应试:

这个原则针对的是基于散列的集合,HashMap,HashSet。

1.不重写会导致混淆,一个hashmap中有两个new Id(3)equals相等,但hashcode不等,则散列表会存储两个值相同的对象,从而导致混淆,从设计来说这是不合理的。

2.使用散列表来get时,会返回null。不同hashcode-》不在同一个散列桶中,找不到返回null (即使在同一个桶里,hashmap里有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也就不再去检验对象的等同性)

3.疑问,假设刚好hashcode相等,equals相等的情况下,没有重写hashcode,是否get能get到值。?存放时是否会把原有的替换掉?

深入:

首先要知道==这个原则针对的是基于散列的集合,HashMap,HashSet。==

然后了解为什么要重写equals:

 HashMap<Id,String> hs  =new HashMap<>();
       hs.put(new Id(3),"lixin"); //构造传递的是int值,id
       hs.put(new Id(3),"age");
for (String value : hs.values()) {
            System.out.println(value);  //age,lixin
        }
        System.out.println(hs.get(new Id(3))); //null

在不重写equals的情况下,hashmap的get始终是返回null,因为

new Id(3) !=new Id(3);->hashcode不等,直接找不到

接下来我们重写equals ,另id相等的同类对象equals返回true。 发现get返回还是null,这是因为基于散列的集合首先是根据散列码来找数据的,而没有重写hashcode导致,两个new Id(3)的hashcode不同,所以还是null.

书面解释(javaeffective):put方法将Id(3)存放在一个散列桶中,get方法却在另一个散列桶中查找这个Id(3)(hashcode不同放在另一个桶里了),所以肯定找不到返回null,即使这两个实例正好放在同一个散列桶中,get方法也是返回的null,因为hashmap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也就不再去检验对象的等同性)

在这里插入图片描述

那我只重写hashcode行吗

也不行,因为hashcode相等的两个对象,不一定equals,因为hashcode的范围有限,232-1个,所以当数据很多时,两个不完全不相等的对象,它的hashcode也会相等。所以hashcode相等还会进一步进行equals判断,所以只重写hashcode没有意义。

hashmap为什么线程不安全

在这里插入图片描述

在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;//执行完这里到
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;//到这里之前切换另一个线程 都会导致死循环
                e = next;
            }
        }
    }

另外线程一个执行transfer完毕后变成7.next=3、3.next=null。

重要

//在给newTable[i]赋值之前切换时就会导致重新某个桶的头结点改变,桶head->3->null 而next已经赋值了 next=7,7.next=3,就继续循环结果桶head->7-》3,然后在循环next=null, 桶head->3->7,7->3。5在线程a这里丢失了(但实际线程b已经保存进去了),且使用get方法时出现死循环

//而赋值后,next=7,执行下去,桶head->7 ,在循环桶head-》3-》7 正常

在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

  1. p = tab[i = (n - 1) & hash]) == null 在判断桶空时直接插入会覆盖

  2. 在插入后给size自增前,另一个线程访问也会导致size大小不正确

    if (++size > threshold)
        resize();
    

多线程下hashmap解决方案

1.collections.synchronizedMap(hashmap);

2.hashtable(所有方法都是synchronized)

3.concurrentHashMap 效率高

concurrentHashMap的研究 后续看看

https://blog.csdn.net/qq_37687656/article/details/83511943

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值