面试汇总-Java基础-集合框架

目录

1、List

1.1、数组和ArrayList的区别?Arraylist是如何扩容的?

1.2、如何保证ArrayList的线程安全?

1.2.1、Vector

1.2.2、SynchronizedList

1.2.3、CopyOnWriteArrayList

 1.3、ArrayList新增元素过程

 2、Map

2.1、Hash冲突的解决方案

2.2、HashMap遇见哈希冲突会如何怎么办?HashMap是线程安全的吗?

2.3、HashMap在高并发下会有什么问题?

2.3.1、循环链表(死循环)

2.3.2、fail-fast

2.4、JDK7 HashMap扩容的两个条件

2.5、JDK8 HashMap中链表转换成红黑树

2.5.1、何时转换

2.5.2、为什么两个阈值分别为6和8?

2.5.3、为什么要转换成红黑树?

2.6、HashMap在JDK7和JDK8中的区别

2.7、红黑树(二分查找)

2.7.1、二叉树、平衡二叉树

2.7.2、红黑树

2.8、Hashmap的底层实现

2.9、HashSet和TreeSet

2.10、ConcurrentHashMap

​​​​​​​2.10.1、ConcurrentHashMap原理

2.10.2、​​​​​​​ConcurrentHashMap不支持key或者value为null的原因

2.10.3、为什么使用synchronized(JDK8)替换ReentrantLock(JDK7)

2.11、​​​​​​​HashMap的容量为什么推荐是2的幂次方?


1、List

1.1、数组和ArrayList的区别?Arraylist是如何扩容的?

空间大小:数组大小是固定的,ArrayList大小是可动态变化的;

存储内容:数组可包含基本数据类型和对象,而ArrayList只可包含对象;

效率:数组比ArrayList更高效。ArrayList的数组扩容影响效率。

ArrayList底层是数组,扩容时,newCapacity = oldCapacity+(oldCapacity >> 1),即扩容到当前大小的1.5倍,再将原数组数组转移到新数组。

1.2、如何保证ArrayList的线程安全?

1.2.1、Vector

        把ArrayList 的所有方法都加上synchronized,性能低。

1.2.2、SynchronizedList

        Collections.synchronizedList把一个普通的ArrayList包装成一个线程安全的数组容器,原理也是给所有的方法加上synchronized。

1.2.3、CopyOnWriteArrayList

        读写分离,常用于读多写少(写入时拷贝数组很耗时)的场景,读操作不加锁,给写操作加上可重入锁。

        CopyOnWriteArrayList有一个array数组和一个可重入锁lock,array数组用于存放具体元素,lock用于在执行修改操作时对修改操作进行加锁。

        加锁后,先拷贝一份存储数组,然后添加元素到拷贝的数组中,然后用拷贝的数组替换原数组。

 1.3、ArrayList新增元素过程

  1. 新增前,先校验ArrayList中的数组长度是否够用,如果不够用,则进行扩容;
  2. 扩容时,生成一个原始数组容量大小1.5倍的新数组,然后将原始数组中的数据通过Arrays.copy复制到新数组中;
  3. 将新增元素添加到新数组中的相应位置。

 2、Map

2.1、Hash冲突的解决方案

开放地址法:探测序列,查找一个空的单元插入。

再哈希法:准备多个hash函数,当发生冲突时,依次使用下一个hash函数。

链地址法:对于相同的hash值,使用链表进行连接。

2.2、HashMap遇见哈希冲突会如何怎么办?HashMap是线程安全的吗?

        HashMap结构由桶加链表组成,桶是一个存储hash值的数组,每当添加一个新元素到HashMap中时,首先会根据hash算法计算出一个hash值,然后根据hash值找到对应的桶的位置,将该值存入桶对应的链表中。在java8之后,若链表大小超过8后,会将桶对应的链表转换成红黑树,以提高查询效率。

        HashMap不是线程安全的。

2.3、HashMap在高并发下会有什么问题?

2.3.1、循环链表(死循环)

        原因:并发+头插

        Jdk1.7在高并发下,HashMap产生死循环,造成CPU100%负载:HashMap在存储时,若size超过当前最大容量*负载因子时,会增加桶的数目,进行HashMap数组扩容(resize()),在resize过程中,会调用transfer()方法将链表转换成新链表,在多线程情况下可能导致链表回路,从而导致死循环。

        并发扩容操作如下:

扩容时,会调用transfer方法将旧的元素重新hash后放到新的table中。

Jdk1.8中HashMap的put元素操作由1.7的头插改为尾插,避免了死循环问题。

2.3.2、fail-fast

        在高并发下,HashMap触发fail-fast:一个线程利用迭代器进行Map遍历时,另一个线程做插入删除操作,造成迭代的fail-fast。

2.4、JDK7 HashMap扩容的两个条件

  1. 当前数据存储的数量(size())必须大于等于阈值(默认大小为16,负载因子0.75,阈值12);
  2. 加入的数据发生了hash碰撞,如果加入的数据未发生hash碰撞,就算存储数量超过了阈值,也有可能不会扩容。

2.5、JDK8 HashMap中链表转换成红黑树

2.5.1、何时转换

        当链表长度到达阈值8时,如果HashMap的数组大小<64,则进行扩容;如果数组大小超过了64,则将链表转换成红黑树

        当链表长度回退到阈值6时,则将红黑树回转成链表。

2.5.2、为什么两个阈值分别为6和8?

        根据泊松分布算出,当链表中的元素个数为8时,出现的几率是亿分之六,基本上可忽略不计;

        红黑树的转换需要时间和空间,为了防止HashMap在链表和红黑树之间频繁转换,故设置高于8转成红黑树、低于6转成链表,中间留了一个7作为缓冲。

2.5.3、为什么要转换成红黑树?

        链表是线性查找,平均时间复杂度为n/2;红黑树为二分查找,平均时间复杂度为log2(n)。

        举例:若hash算法写的不好,一个桶中冲突1024个key,使用链表平均需要查询512次,但是红黑树仅仅10次,红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能。

​​​​​​​2.6、HashMap在JDK7和JDK8中的区别

  • 底层结构

        JDK7:数组+链表,Entry。JDK8:数组+链表/红黑树,Node。

  • 哈希表为空时

        JDK7:使用inflateTable()初始化一个数组。JDK8:直接调用resize()扩容。

  • Hash冲突时插入数据

        JDK7:头插(高并发下可能会发生死循环)。JDK8:尾插。

  • Hash函数

        JDK7:直接使用key的hashCode值,扰动复杂(对hash值一共进行5次异或和4次右移)。

        JDK8:(h = key.hashCode()) ^ (h >>> 16),结果更散列、扰动简单。

  • 扩容策略

        JDK7:重新计算hash。

        JDK8 :e.hash & oldCap,如果结果为0,位置不变;不为0,新的位置为老位置+老数组长度。

2.7、红黑树(二分查找)

2.7.1、二叉树、平衡二叉树

  • 二叉树

        每个节点下有1个或者2个子节点,左节点<=父节点<=右节点,最大查找长度为树的高度。

但是在极端情况下,二叉树可能会退化为链表。

  • 平衡二叉树(AVL树)

        在二叉树的基础上,每次新增节点的时候,都会通过左旋和右旋操作,来保证左子树和右子树的高度差不超过1,防止二叉树退化成链表。

 

2.7.2、红黑树

         是一种平衡二叉树,它有以下规则:

  1. 每一个节点,要么是黑色的,要么是红色的;
  2. 根节点是黑色的;
  3. 叶子节点(空)是黑色的;
  4. 红色节点不能同时存在;
  5. 从一个节点到它的任意根节点,所经过的黑色节点数必须是相同的。

        在插入节点时,会通过变色和旋转两种操作,来保证红黑树满足上述五个条件。

        红黑树是一种弱平衡二叉树,相对于平衡二叉树,插入和删除时旋转次数少,所以在插入和删除较多的情况下,使用红黑树;而在修改少,查找次数多的情况下,使用平衡二叉树。

 

​​​​​​​2.8、Hashmap的底层实现

Jdk1.7:数组+链表

Jdk1.8:数组+链表/红黑树

2.9、HashSet和TreeSet

 hashSet底层是基于HashMap实现的。

HashSet:无序,由哈希表实现,支持null,较快。

TreeSet:有序,由树实现,不支持null,较慢。

2.10、ConcurrentHashMap

        HashMap在并发时不是线程安全的;

        HashTable通过synchronized来保证线程安全,每个map只有一把锁,效率低下;

        通过Collectrions.synchronizedMap(map类型对象)进行加锁,也是锁全表,效率低下。

​​​​​​​2.10.1、ConcurrentHashMap原理

  •         jdk1.7:segment数组(存储锁)+hashEntry数组,segment继承自ReentrantLock,采用锁分段技术来保证线程安全,将数据分为一段一段的,给每段数据各分配一把锁,不同数据段之间的读写互不影响,效率高;

  •         jdk1.8:Node数组+链表/红黑树,并发控制使用 synchronized 和 CAS 来操作。
    •         synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
      •         Node和HashEntry的元素value和指针next时用volatile修饰的,所以在多线程环境下线程A修改节点的value或者新增节点的时候是对线程B可见的。所以HashMap的get方法不需要加锁。

2.10.2、​​​​​​​ConcurrentHashMap不支持key或者value为null的原因

  • 不支持key为null:没有原因
  • 不支持value为null:多线程环境下,如果map.get(key)返回null,就无法判断,究竟是map中没有key,还是该key对应的值就为null。

2.10.3、为什么使用synchronized(JDK8)替换ReentrantLock(JDK7)

        Synchronized性能提升:JDK6的锁优化,无锁->偏向锁->轻量级锁->重量级锁,锁粗化、所消除。

        并发能力更高:CAS + synchronized锁每个链表的头节点;segment锁多个链表。

2.11、​​​​​​​HashMap的容量为什么推荐是2的幂次方?

计算桶的数组下标算法为:i = (n - 1) & hash。

桶的位置i = (n - 1) & hash  与操作可以实现HashMap中的数据均匀分布

实现均匀分布可以采用%运算hash%length,但%运算效率低下,所以采用2进制运算(位运算)hash&(length-1),可保证元素在桶数组中的均匀分布

当n为2的幂次方时,hash%length与hash&(length-1)效果相同,但&速度更快。

以上内容为个人学习汇总,仅供学习参考,如有问题,欢迎在评论区指出,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值