java-集合面试题

Java常见的集合类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q3HaBBes-1682084792927)(F:\JavaLearningMaterial\笔记\img\Java集合框架体系.png)]

  • Map接口和Collection接口是所有集合框架的父接口
  • Map接口的实现类主要有HashMap、TreeMap、ConCurrentMap、HashTable、Properties
  • Collection接口的实现接口主要有Set接口、List接口
    • Set接口的主要实现类有HashSet、LinkedHashSet、TreeSet
    • List接口的主要实现类有ArrayList、LinkedList、Vector以及Stack

对ArrayList的理解

底层数据结构:ArrayList底层是基于数组实现的,因为在java中数组需要在定义时指定数组的长度,所以对于长度不确定的情况就需要预估数组长度,而长度不足时就需要进行扩容,即申请一块更大的连续存储空间作为新的数组,并将原数组中的数据拷贝过来,再回收原数组空间,由于在开发中这种情况十分常见,ArrayList对这部分重复的逻辑代码进行了封装,实现了动态扩容的效果.

线程安全:ArrayList不是线程安全的,只能用于单线程情况下,多线程情况下可以使用Collections.synchronizedList(List l)函数返回一个线程安全的List类,或者使用concurrent并发包下的CopyOnWriteArrayList类

扩容机制:默认情况下创建ArrayList集合,集合初始长度为10,eg:List list = new ArrayList<>();
若创建ArrayList集合时指定了初始长度,则集合初始长度为指定值,eg:List list = new ArrayList<>(15);

jdk1.7与jdk1.8的区别:1.7创建ArrayList对象时就初始化数组,1.8采用懒加载,第一次添加(add)元素时才初始化数组

开发中建议预估长度,指定初始化容量:List list = new ArrayList<>(int initialCapacity);

对LinkedList的理解

底层数据结构:LinkedList底层是使用双向链表实现的,每个节点保存了前一个节点和后一个节点的地址,因此LinkedList集合的存储空间不要求是连续的,且在内存充足的情况下可以无限延展,同时,LinkedList集合实现了栈和队列的操作方法,因此可以当作栈,队列以及双端队列来使用

**线程安全:**LinkedList同样不是线程安全的,只能在单线程下使用,多线程情况可是考虑使用Collections.synchronizedList(List list)函数返回一个线程安全的List类,或者使用concurrent并发包下的ConcurrentLinkedQueue类

**扩容机制:**不需要扩容

ArrayList与LinkedList的区别

**数据结构:**ArrayList是基于动态数组实现的,LinkedList是基于双向链表结构实现的

**随机访问效率:**ArrayList的随机访问效率比LinkedList要高,因为LinkedList是线性存储结构,访问时需要移动指针从前往后一个个去访问查找,而ArrayList可使用数组索引直接获取

**增加和删除效率:**在非首尾的增加和删除情况下,LinkedList比ArrayList的效率高,因为ArrayList增删时需要移动集合中的其他数据

**内存空间占用:**LinkedList内存占用更大,因为LinkedList每个节点除了保存数据之外还存储了两个指针,一个指向前一个节点,一个指向后一个节点

**线程安全:**ArrayList和LinkedList都是不同步的,即非线程安全的.

如何解决ArrayList和LinkedList的线程安全问题

一般有两种解决方案:

第一:我们优先在方法里面使用集合,定义为局部变量,就不会有线程安全的问题

第二:对于ArrayList,可以考虑使用Collections的synchronizedList方法将ArrayList转换成一个线程安全的容器后再使用,或者使用concurrent并发包下的CopyOnWriteArrayList类,对于LinkedList,同样可以考虑使用Collectons类下的synchronizedList()方法返回一个线程安全的List在使用,也可以使用concurrent并发包下线程安全的ConcurrentLinkedQueue类替代

如何实现数组和List之间的转换

String[] strings = {"aaa", "bbb","ccc"};
//数组转集合,使用JDK中的java.util.Arrays工具类中的asList方法
List<String> stringList = Arrays.asList(strings);
stringList.forEach(System.out::println);

//集合转数组,使用List集合的toArray方法
String [] strings1 = stringList.toArray(new String[stringList.size()]);
for (String s : strings1) {
    System.out.println(s);
}

HashMap

实现原理

当我们往集合中添加元素的时候,会根据所添加元素的键值来计算hash值,然后根据hash值计算出元素在数组中的索引,接着判断该索引位置是否有值,如果没有,直接将元素插入进去,如果有,就需要比较数组中元素与新添加的元素的键是否相等,相等则直接覆盖数组中元素的值,不相等则将新添加的元素挂在数组中元素的下面,形成链表

在JDK7和JDK8中,HashMap的底层实现是不一样的,JDk7底层使用的是拉链法解决哈希冲突,即使用数组和链表组合,每个数组位置都存放一个链表,若遇到hash冲突,直接将元素加入链表即可

JDK8使用的数组+链表+红黑树的结构实现,当链表中的元素个数达到阈值(默认8个)时,会将链表转换为红黑树,以减少查询时间,扩容resize()时,红黑树拆分形成的树的节点数小于等于6个就会退化成链表

put方法执行流程

添加元素时首先会对集合中的数组判空,
若为空则将其初始化为一个容量为16的数组,
不为空则使用所添加元素的key值计算其hash值,根据hash值计算得到元素在数组中的位置索引

然后判断该位置是否有元素存在,
若没有,将添加的键值对存入其中,
若有,将该元素的key值与要添加的元素的key进行比较,
若相等,则进行值覆盖操作,
若不相等,则先判断当前数组中的这个元素是否为红黑树的节点,
若是,则将要添加的元素按照红黑树规则添加到红黑树中去,
若不是,则遍历该数组位置的链表,将每个元素的key与要添加元素的key做对比,
若key相同则做值覆盖操作,
若遍历完链表仍未找到key相同的元素,就将要添加的元素挂在链表尾部,并判断此时链表上的元素是否达到阈值,达到了就转为红黑树,最后将集合元素加一,判断数组是否达到扩容阈值,达到则进行扩容

HashMap的寻址算法

HashMap底层调用hash方法计算key的hash值,hash方法会对传入的键值判空,为空则返回0,不为空则调用hashCode方法获取散列值,记为h,并将h和h无符号右移16位后的值做异或运算,返回得到的值

而后使用该值与数组长度减一后的值做与运算,得到数组索引

HashMap扩容机制

在JDK8中,HashMap集合在第一次添加元素时会使用resize方法进行初始化扩容,此时生成一个长度为16的数组

当某个数组空间上的链表元素达到了8个,而数组长度尚未超过64时,也会对数组进行扩容

之后在数组中元素达到扩容阈值,即数组长度的75%时会将数组长度扩容为原来的两倍,扩容的同时,数组内的元素也需要重新分发,这也是JDK8优化的一个地方

JDK7时,扩容后需要对所有元素重新计算hash值,并根据hash值进行分发

JDK8对此进行了优化,扩容后根据元素hash值与数组扩容前容量做位与操作(e.hash&oldCap)的值是否为0得到新的索引位置,为0则该元素在新数组中的索引与在旧数组中的索引相同,不为0则该元素在新数组中的索引为在旧数组中的索引+旧数组的长度

为何HashMap的数组长度一定是2的整次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

HashMap在JDK7时的多线程死循环情况

JDK7中的HashMap底层使用数组与链表实现,因为使用的是头插法,多线程情况下,数组扩容后进行数据迁移时就可能会出现死循环的情况.

假设线程A已对做了扩容,正要迁移一条链表上的两个元素1->2,此时线程B介入,线程A被阻塞,线程B直接将数组扩容并将数组迁移到了新数组当中,因为使用的是头插法,迁移后元素顺序颠倒,变成了2->1;然后线程A苏醒,使用头插法迁移数据到新数组,使得元素形成了1->2->1死循环

JDK8对此作了优化,不再使用头插法迁移数据,改用尾插法,避免了死循环

HashSet与HashMap的区别

HashSet实现了Set接口,进存储对象,HashMap接口实现了Map接口,存储的是键值对

HashSet底层其实是用HashMap实现存储的,HashSet底层封装了一系列HashMap的方法,以HashMap的键存储元素值,value值默认为Object对象,所以HashSet不允许重复元素存在,判断的标准与HashMap一致,都是通过比较两个键的hash值以及通过equals方法比较键值是否相等

TreeMap添加元素的时候,见是否需要重写hashCode和equals方法?

不需要,TreeMap底层源码并未使用hashCode和equals方法.仅HashMap的键依赖于键的hashCode和equals方法实现键唯一,若HashMap的键是自定义对象,就必须要重写hashCode和equals方法

HashMap在JDK8加入了红黑树结构,那么HashMap的见是否需要实现Comparable接口或传递比较器对象?

不需要,因为HashMap底层默认是使用键的hash值大小关系来创建红黑树

TreeMap和HashMap的效率谁更高?

考虑最坏的情况,如此时添加了8个元素,这8个元素在HashMap中形成了一个链表,那么此时TreeMap的效率更高,但这种极端情况出现较少,故一般情况下还是HashMap的效率更高

Map集合中,java会提供一个键重复了,不覆盖put的方法吗?

会,如HashMap中的putIfAbsent方法

思想:代码中的逻辑具有两面性,如果我们知道了其中的A面,而且代码中还有变量可以控制其两面性,则该逻辑一定会存在B面

习惯:
使用boolean类型的变量进行控制时,一般只有AB两面,对应boolean的两个值
使用int类型变量进行控制时,一般至少存在三面,因为int可以取多个值,如TreeMap插入时的比较返回值就是int型,其正数,负数,0分别代表三种不同的情况

三种双列集合,如何选择?

HashMap,LinkedHashMap,TreeMap

默认情况下使用HashMap,因其效率最高

若需要存取有序,则使用LinkedHashMap

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值