史上最全Map问题秘籍

1.==与equals的区别以及底层的实现

java是一门面向对象的语言,每创建一个对象就会在jvm堆空间中开辟一块空间,并有一个地址值指向他,在java中,==比较的是内存地址,如果内存地址相同,则这两个值相同,而equals需要根据是否重写equals方法来比较,如果重写了equals方法,则比较的是内容,内容相等,则值相等,如果为重写equals方法,那么比较的是地址值,equals的底层便是通过==比较的,详情见下方源码。

2.为什么重写equals方法还要重写hashcode方法

        hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度。如果只重写equals方法,不重写hashCode方法,就有可能导致a.equals(b)这个表达式成立,但是hashCode却不同。会造成一个完全相同的对象会存储在hash表的不同位置.

3.jdk1.7和jdk1.8中底层结构

再说底层结构时我们先了解一下简单的数据结构数组和链表

数组:相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标

数组的特性:

1.数组必须先定义固定长度,不能适应数据动态增减

2.当数据增加时,可能超出原先定义的元素个数,当数据减少时,造成内存浪费

3.数组查询比较方便,根据下标就可以直接找到元素,时间复杂度O(1);增加和删除比较复杂,需要移动操作数所在位置后的所有数据,时间复杂度为O(N)

链表:是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表的特性:

1.链表动态进行存储分配,可适应数据动态增减

2.插入、删除数据比较方便,时间复杂度O(1);查询必须从头开始找起,十分麻烦,时间复杂度O(N)

常见的链表:

1.单链表:通常链表每一个元素都要保存一个指向下一个元素的指针

2.双链表:每个元素既要保存到下一个元素的指针,还要保存一个上一个元素的指针

3.循环链表:在最后一个元素中下一个元素指针指向首元素

链表和数组都是在堆里分配内存

应用:

如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反, 如果需要经常插入和删除元素就需要用链表数据结构了

在jdk7中hashMap的底层是通过数组+链表的方式实现的,由于每一次的hash碰撞,会将hashcode值相同,key不同的元素放在一个链表中,我们都知道链表有一个缺点,如果查询,需要从前遍历到最后进行检索查询,时间复杂度为o(n),效率非常缓慢,因此jdk8中为了解决链表过长而导致的性能问题,引入的红黑树。

jdk7:数组+链表

jdk8:数组+链表(红黑树)

4.链表和红黑树什么时候进行相互转换

        在jdk8中,需要链表长度大于8并且数组长度大于64两个条件同时满足才会转换为红黑树,因为,当红黑树的节点个数小于6的时候又会将红黑树转换为链表。

5.为什么两个条件同时满足才会转换为红黑树

        当HashMap中的链表长度大于8时,查找元素的时间复杂度会变高,因为需要遍历链表来查找元素。而树化后,查找元素的时间复杂度会变为O(log n),更加高效。但是,树化也需要消耗额外的内存空间,因此只有在链表长度大于8时才进行树化,以平衡时间和空间的消耗。而数组长度大于64是为了保证树化后的树仍然具有较好的性能,因为树的高度与元素数量有关,而数组长度大于64时,树的高度不会超过6,性能较好。

6.hashMap如何避免内存泄漏问题

        自定义对象作为key时,必须重写equals和hashCode方法。如果两个对象内容相同但没有重写equals和hashCode方法,比较结果是不相等的,在HashMap中存储的时候会导致内存堆积而造成内存泄漏问题。因此,重写equals和hashCode方法可以确保HashMap中的key值唯一,避免内存泄漏问题的发生。另外,阿里巴巴开发手册中也要求:只要覆写equals,就必须覆写hashCode。

7.hashMap如何降低hash冲突概率

        HashMap使用两次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均。在HashMap中,第一次扰动函数是通过key的hashCode()方法计算得到的,第二次扰动函数是通过对第一次扰动函数的结果进行位运算得到的。具体来说,第二次扰动函数会将第一次扰动函数的结果右移16位,然后将这个结果与第一次扰动函数的结果进行异或运算,得到最终的哈希值。这样做的目的是为了让高位和低位都参与到哈希值的计算中,从而降低哈希冲突的概率。

        另外,当HashMap中出现哈希冲突时,它会采用链式寻址法来解决。对于存在冲突的key,HashMap会把这些key组成一个单向链表,然后采用尾插法把这个key保存到链表的尾部

8.为什么hash值通过高16位与低16位进行异或运算

        hash值通过高16位与低16位进行异或运算是为了降低hash冲突的概率。由于n的值一般小于2^16,因此i的值始终是用hash值的低16位来参与运算的,高位都用不到,这就造成大量的key可能集中存储在固定的几个数组位置,影响查找性能。通过位移运算,使通过hashcode方法得到的hash值本身和它右移16位后的hash值做异或运算,相当于把高位和低位的特征进行了组合,这样通过高位和低位组合后的hashCode通过 & 运算符进行运算后,它得到的一个数组的位置的散列度一定会更高,从而降低hash冲突的概率。

9.hashmap中key为null存放在数组的什么位置

索引为0的位置

10hashmap如何实现数组扩容问题

        HashMap在实现数组扩容时,按照2的n次幂进行扩容,会创建一个新的数组,并将原来数组中的元素重新计算哈希值后,放入新数组中。具体步骤如下:

  1. 当HashMap中的元素个数超过了负载因子(默认为0.75)与当前数组容量的乘积时,就会触发数组扩容操作。

  2. 创建一个新的数组,其容量是原数组的两倍。

  3. 遍历原数组中的每个元素,重新计算其哈希值,并根据新数组的容量计算出在新数组中的索引位置。

  4. 将原数组中的元素按照计算出的索引位置,放入新数组中。

  5. 将新数组设置为HashMap的内部数组。

通过这样的方式,HashMap实现了数组的扩容。扩容操作可以有效避免哈希冲突的发生,提高了HashMap的性能。

11.hashmap底层采用的是单链表还是双链表

        HashMap底层采用的是单链表和红黑树的结合,也就是说,当链表长度超过一定阈值(默认为8)时,会将链表转换为红黑树,以提高查找效率。这种结合的方式称为链表散列。在JDK8之前,HashMap底层只采用单链表的方式。

12.hashMap根据key查询的复杂度

        HashMap根据key查询的复杂度是O(1),即常数时间复杂度。因为HashMap内部是通过哈希函数将key映射到一个桶中,然后在桶中查找对应的value,这个过程的时间复杂度与HashMap中的元素个数无关,只与桶的数量有关。在理想情况下,每个桶中只有一个元素,这样查找的时间复杂度就是O(1)。在最坏情况下,所有元素都被映射到同一个桶中,此时查找的时间复杂度为O(n),但这种情况很少出现。

13.hashMap1.8如何避免多线程死循环问题

        HashMap 1.8并没有直接解决多线程环境下的死循环问题。在多线程环境下,如果多个线程同时进行put操作并发地修改HashMap,可能会导致链表或红黑树的结构被破坏,进而导致死循环或数据丢失等问题。

        为了避免这种问题,推荐使用ConcurrentHashMap,它是Java提供的线程安全的哈希表实现。ConcurrentHashMap在内部使用了一种叫做分段锁(Segment)的机制,将整个哈希表分成多个小的段,每个段都有自己的锁,不同的段可以同时被不同的线程访问。

通过分段锁的机制,ConcurrentHashMap可以支持多线程并发地进行put、get等操作,而不需要对整个哈希表进行加锁,从而提高了并发性能。同时,ConcurrentHashMap也提供了一致性的迭代器,能够在多线程环境下安全地进行遍历操作。

总结起来,如果需要在多线程环境下使用哈希表,推荐使用ConcurrentHashMap来避免多线程死循环问题。而HashMap在多线程环境下并不是线程安全的,需要通过外部同步控制来保证线程安全。

14.为什么hashMap1.8引入了红黑树

        HashMap 1.8引入了红黑树是为了解决HashMap在处理大量数据时,发生哈希冲突导致链表过长而造成的性能下降问题。在HashMap中,当多个元素映射到同一个桶(bucket)时,会以链表的形式存储在同一个桶中。当链表过长时,查找元素的效率会降低,因为需要遍历整个链表才能找到目标元素。

        红黑树是一种平衡二叉搜索树,具有较快的查找、插入和删除操作的时间复杂度。当链表长度超过一定阈值(默认为8)时,HashMap会将链表转换为红黑树,以提高查找元素的效率。通过红黑树的特性,可以将查找元素的时间复杂度从O(n)降低到O(log n),从而提高了HashMap的性能。

        引入红黑树的另一个原因是为了解决哈希碰撞攻击(Hash Collision Attack)。哈希碰撞攻击是一种恶意攻击手段,通过故意构造大量哈希冲突的数据,使得HashMap的链表长度过长,从而导致查找元素的效率极低。通过引入红黑树,可以将链表长度控制在一定范围内,防止哈希碰撞攻击。

        总之,HashMap 1.8引入红黑树是为了提高HashMap在处理大量数据时的性能,并增强其安全性。

15.为什么负载因子为0.75而不是1

        在hashmap负载因子熟悉上方注释可以看到,采用0.75作为负载因子是为了平衡时间和空间效率,假如负载因子为1,意味着当全部数组容量满才会触发扩容导致效率低下,假如负载因子为0.5或者更小,那么会存在很大一部分内存空间不能利用,导致资源浪费,反复研究测试,采用0.75可以更好的平衡时空效率,提高hashMap性能,因此,hashmap负载因子采用了0.75。

16.HashMap如何存放1万条数据,效率更高,为什么?

        对于存放1万条数据,使用HashMap确实可以提高效率,但是需要注意以下几点:

  1. 初始容量:在创建HashMap时,可以指定初始容量,建议根据实际情况合理设置。如果初始容量过小,可能会导致频繁的扩容操作,影响效率;如果初始容量过大,可能会浪费内存空间。

  2. 负载因子:负载因子是指哈希表中元素的数量与容量的比值,默认为0.75。当哈希表中的元素数量达到容量和负载因子的乘积时,会触发扩容操作。可以根据实际情况调整负载因子,以平衡内存占用和性能。

  3. 哈希冲突:在哈希表中,不同的键可能会映射到相同的索引位置,这就是哈希冲突。HashMap使用链表或红黑树来解决哈希冲突,当链表长度超过阈值时,会将链表转换为红黑树,以提高查找效率。可以通过合理设置初始容量和负载因子来减少哈希冲突的概率。

  4. 键的类型:HashMap中的键需要正确实现hashCode()和equals()方法,以保证键的唯一性和正确的查找操作。如果键的hashCode()方法实现不好,可能会导致哈希冲突增多,影响性能。

总结来说,虽然HashMap在存放1万条数据时可以提高效率,但需要注意初始容量、负载因子、哈希冲突和键的类型等因素,以充分发挥HashMap的优势。

17.为什么不直接将key作为hash值而是与高16位进行运算?

        在HashMap的实现中,将key的hash值与高16位进行运算是为了增加散列性,减少哈希冲突的概率。

        HashMap内部使用一个哈希函数来将key映射到一个桶(bucket)的索引位置。在计算哈希值时,简单地将key直接作为哈希值可能会导致哈希冲突较多,即不同的key计算得到相同的哈希值。这样会导致在桶中存放的链表或红黑树的长度增加,查找效率降低。

        为了减少哈希冲突,HashMap的哈希函数会将key的哈希值与高16位进行异或运算。这样做的目的是将高16位的信息混入到低16位中,增加哈希值的随机性,从而减少哈希冲突的概率。这样能够更均匀地分布数据,提高HashMap的性能。

        总结来说,将key的hash值与高16位进行运算是为了增加散列性,减少哈希冲突的概率,提高HashMap的性能和效率。

18.HashMap的底层是有序存放的吗

        HashMap的底层存储结构是无序的,即HashMap不保证元素的顺序。在HashMap中,元素的存放是根据键的哈希值来确定的,而不是根据插入的顺序。

19.LinkedHashMap和treeMap底层是时如何实现有序地

LinkedHashMap和TreeMap都是实现了有序存储的Map。

  1. LinkedHashMap:LinkedHashMap底层使用的是HashMap和双向链表的结合。HashMap负责存储键值对,并且根据键的哈希值来确定存储位置。双向链表则负责维护插入顺序或者访问顺序。通过双向链表,LinkedHashMap可以保持插入的顺序或者根据访问的顺序进行排序。

  2. TreeMap:TreeMap底层使用的是红黑树(Red-Black Tree)。红黑树是一种自平衡的二叉搜索树,它可以保持元素的有序性。TreeMap根据键的比较结果来构建和维护红黑树,从而实现对键的有序存储。

LinkedHashMap通过双向链表来保持插入顺序或者访问顺序,而TreeMap通过红黑树来实现键的有序存储。因此,当需要有序存储的功能时,可以选择使用LinkedHashMap或TreeMap,具体选择哪个取决于需求和使用场景。

20.如何在高并发的情况下使用HashMap

在高并发的情况下使用HashMap需要注意以下几点:

1.使用ConcurrentHashMap代替HashMap

ConcurrentHashMap是线程安全的HashMap,它使用了分段锁的机制,可以在高并发的情况下提高并发性能。在使用ConcurrentHashMap时,需要根据业务需求选择合适的并发级别。

2.尽量减少对HashMap的修改操作

在高并发的情况下,对HashMap的修改操作会导致线程竞争,降低并发性能。因此,尽量减少对HashMap的修改操作,例如可以使用不可变对象作为HashMap的键或值。

3.使用合适的初始容量和负载因子

在创建HashMap时,需要根据业务需求选择合适的初始容量和负载因子。如果容量过小,会导致频繁的扩容操作,影响并发性能;如果容量过大,会浪费内存资源。负载因子越小,HashMap的性能越好,但是会占用更多的内存空间。

4.使用线程安全的迭代器

在遍历HashMap时,需要使用线程安全的迭代器,例如使用ConcurrentHashMap的迭代器或者使用Collections.synchronizedMap方法包装HashMap。

总之,在高并发的情况下使用HashMap需要注意线程安全和性能问题,需要根据业务需求选择合适的并发容器和策略。

21.concurrentHashMap底层实现原理

        ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它的底层实现原理主要包括以下几个方面:

  1. 分段锁机制:ConcurrentHashMap将整个哈希表分成多个段(Segment),每个段维护着一部分键值对。每个段都有自己的锁,不同的段可以同时被不同的线程访问,从而提高并发性能。当一个线程访问某个段时,只会锁定该段,不会影响其他段的并发操作。

  2. Hash表结构:每个段内部使用一个类似于HashMap的结构来存储键值对。每个键值对被存储在一个Entry对象中,多个Entry对象通过链表形式连接在一起,形成一个桶(Bucket)。当多个键值对的哈希值相同时,它们会被放置在同一个桶中,通过链表形式进行冲突解决。

  3. 锁的粒度:ConcurrentHashMap的锁粒度相对较细,每个段都有自己的锁,因此不同的线程可以同时访问不同的段。这样可以提高并发性能,避免了对整个哈希表的锁定。

  4. 并发控制:ConcurrentHashMap使用了一些特殊的算法和技术,如CAS(Compare and Swap)操作和volatile关键字,来实现对并发访问的控制。CAS操作可以实现非阻塞的原子性操作,而volatile关键字可以保证可见性,使得不同线程之间的操作能够及时感知到对共享变量的修改。

总的来说,ConcurrentHashMap通过分段锁机制和细粒度的并发控制实现了高效的并发访问。它在多线程环境下提供了高性能的并发操作,适用于高并发的场景。

22.concurrenHashMap1.7和1.8加锁区别

jdk1.7:segment数组+lock锁

jdk1.8:cas+synchronized

23.concurrenHashMap中put原理

1.根据扰动函数计算出key的hash值

2.进入死循环(乐观锁)判断数组是否初始化,如果未初始化执行initTab进行初始化数组,进入下一次死循环;

3.根据hash值使用寻址算法计算出数组索引位置,判断数组索引位置是否存在元素,如果不存在则使用cas进行添加新值并进入下一次for循环

4.判断数组是否正在扩容,并帮助数组进行扩容

5.根据hash值是否冲突进行覆盖和新增操作;

6.put结束

24.hashTable和hashMap的区别

1.线程安全性不同:hashTable使用了大量的synchronized关键字,线程安全,HashMap线程不安全

2.hashTable的key不能为null,hashMap的key可以为null,并且存储在数组的0索引位置

3.hashTable的初始容量为11,hashMap的初始容量为16,并在第一次put的时候进行延迟加载

4.hashTable扩容2n+1,hashMap扩容为原来的两倍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值