当前位置: 首页 > news >正文

Java集合面试题(持续更新)

概念

数组和集合的区别,你用过哪些

  • 数组是固定的,一旦创建便不可改变。而集合是可变的,可以动态调整元素个数。
  • 数组可以存储基本数据类型和对象类型,但是集合只能存储对象。
  • 数组可以使用下标直接访问元素,而集合需要迭代器或者其他方法访问元素

我用过如下Java集合类:

  • ArrayList:动态数组,实现了List接口,支持动态增长
  • LinkList:双线链表,也实现了List接口,支持快速插入和删除
  • HashMap:基于hash表的map实现,存储key-value,通过key快速查找
  • HashSet:基于hash实现的set集合,存储唯一元素
  • TreeMap:基于红黑树实现的有序map集合,可以按照key的顺序排序
  • LinkHashMap:基于hash和双向链表实现的map集合,保持插入顺序和访问速度
  • ProorityQueue:优先队列,按照比较器或元素的自然顺序进行排序

说说Java中的集合

List

List是有序的Collection,使用这个接口可以精确的控制每个元素的位置,用户可以根据索引访问list中的元素。

  • ArrayList:是容量可变的非线程安全列表,其底层使用的是数组实现,当扩容时,会扩大创建更大的数组,然后将原数组复制到新数组。ArrayList可以对元素快速访问,但是随机插入删除很慢。
  • LinkList:本质是一个双向链表,与ArrayList相比,随机查找比较满,但是随机插入和删除比较快

Set

set不允许存在重复的元素,与list不同,set是无序的。

HashSet:是通过HashMap实现的,HashMap的key就是HashSet存储元素,其中的value值都是一样的,使set内元素保持唯一性。由于hashset由hashmap实现的,因此线程不安全。

LinkHashSet:继承自HashSet,使用双向链表保持元素插入的顺序,

TreeSet:通过TreeMap实现,添加数据按照比较规则将其插入合适的位置,保证插入数据的有序性。

Map

是一个键值对集合,存储键值之间的映射。key无序,要求唯一;value不要求有序,允许重复。map没有继承Collection,从map中获取元素时,只要给出键对象,就会返回相应的value。

HashMap:在Java8之前,hashmap采用数组+链表去实现的,数组是hashmap的主体,链表是为了解决hash冲突存储的(拉链法)。【ps:在Java8之前,当一个key要插入hashmap时,需要根据相应的hash公式计算出key该放在数组的哪个位置,但是数组长度有限,很可能出现撞车现象,那么此时我们在数组内存放一个链表,链表内存储键值对,然后将撞车的key插入到头部。这就是拉链法】。但是在Java8之后发生了改变,当链表长度大于8时,会将链表转化为红黑树。

LinkHashMap:继承自hashmap,底层还是基于链表+数组+红黑树数组。但是在上面结构的情况下,添加了一条双向链表,可以保持插入的顺序。

HashTable:数组+链表组成。

TreeMap:红黑树

ConcurrentHashMap:Node数组+链表+红黑树实现,是线程安全的(Java8之前是Segment锁,1.8之后采用synchronized实现)

Java中的线程安全集合是什么

java.util

vector:线程安全的动态数组,其内部方法基本都是经过synchronize修饰,但是如果不需要线程安全,不建议使用,同步是有额外开销的

HashTable:线程安全的hash表,给每个方法加上synchronized关键字,这样锁住的就是整个table对象。

java.util.concurrent

并发Map:

  • ConcurrentHashMap:他与HashTable的主要区别是二者加锁力度不同,在Java8之前,ConcurrentHashMap加的是分段锁,也就是Segment,每个Segment都含有table的一部分,这样不同分段之间的并发就不受影响。在Java8之后,他取消了Segment字段,直接在table元素上加锁,实现对每一行进行枷锁,进一步减少了并发冲突的概率。对于put操作,如果key对应的数组为null,则通过CAS操作将其设置为当前值,如果key对应的数组元素不为null,则对该元素使用sychronized关键字申请加锁,如果该链表的长度大于8,则将链表转化为红黑树。
  • ConcurrentSkipListMap:基于跳表算法的可排序的并发集合。

并发set:

  • ConcurrentSkipListSet:是线程安全的有序集合,底层是基于ConcurrentSkipListMap实现的
  • ConcurrentOnWriteAtrraySet:是线程安全的set实现,他是线程安全的无序集合。

并发list:

  • CopyOnWriteArrayList:他是ArrayList线程安全的变体,其中所有写操作都通过对底层数组进行全新复制来实现,允许存储null。当对象进行写操作时,使用lock进行同步处理,内部拷贝了原数组,并在新的数组上进行添加操作,最后将新数组覆盖掉旧数组。

Collections和Collention的区别

  • Collection时Java集合类的一个基础接口,他定义了一系列的通用操作和方法,如添加、删除、遍历等,用户同意操作和管理。List、Set都是他的实现类
  • Collentions是Java提供的一个工具类,位于java.util中,他提供了一系列的静态方法,用于对集合进行操作和算法,比如排序、查找、替换等。这些方法可以对实现了Collent的接口进行操作

遍历集合的方法

普通for循环

List<String> list = new ArrayList<>();for(int i=0;i<list.size();i++){}

增强for循环 也即是for-each

//这里不再定义集合,使用list代表集合for(String element:list){//业务操作
}

Iterator迭代器

用来迭代遍历集合,用户要删除元素的状况

//这里不再定义集合了 使用list代替
Iterator<String> iterator = list.iterator();while(iterator.hasNext()){String ele = iterstor.next();//业务操作
}

使用Java8后的Stream API

//这里不再定义listlist.forEach(element->log.info(element))

List

说一下常见List

线程不安全

  • ArrayList:基于动态数组实现,他允许快速的根据下表检索数据,在删除和添加数据时,如果不在末尾或者头部进行操作,那么他需要移动大量元素,性能还是比较低的,适用于频繁的随机访问的场景
  • LinkList:基于双向链表实现,在插入和删除元素时,只要删除元素的指针并释放空间即可,但是随机访问元素时,需要从头部开始遍历,大数据量下,还是比较吃性能的。适用于进行频繁插入删除的场景

线程安全

  • Vector:与ArrayList相同,底层都是由动态数组实现,由于Vector都是同步的,所以可以保持数据的一致性,但是如果在单线程的环境下,可能会增加系统的开销。
  • CopyOnWriteArrayList:在对列表进行更新或修改的时候,底层会创建(复制)一个新的底层数组,在修改、更新时会对数组进行加锁,此时读操作仍在原数组上进行,不会被写操作阻塞,实现了读写分离。更新完毕后,将新数组覆盖旧数组。

List如何一边遍历一边修改

在Java中,如何一边遍历一边修改取决于遍历List的方法。

使用for

        在for循环中,是可以一边遍历和修改的

//这里不再定义List,使用list变量代替for(int i=0;i<list.size();i++){list.set(i,list.get(i)*2)
}

使用foreach

        非常不建议使用foreach进行一边遍历一边修改,这样很很可能会损坏破坏迭代器内部的构造,foreach底层是基于迭代器实现的,在遍历过程中修改会使得迭代器结构和实际结构不一致,会出现非常意外的状况

//这里的List对象才采用list代替for(Object obj:list){//运行到这里时,一定会报错!obj.set(list.indexOf(obj),obj*2+1)
}

使用迭代器

        使用迭代器可以非常完美的进行边遍历边修改,但是修改时要使用迭代器对象,不要使用list对象

//使用list表示List对象Iterator<Integer> iterator = list.iterator();for(iterator.hasNext()){Integer num = iterator.next();//这里注意要使用迭代器对象进行修改iterator.set(num*2);
}

对于线程安全的CopyOnWriteArrayList(),一般来说修改是不会报错的,但是可能会删除旧的数据,因为更新操作是在新数组上进行的。

ArrayList线程安全吗,如何让他变得安全

ArrayList的线程并不是安全的,如果想要让他变成安全可以:

使用Collections 的synchronizedList方法包装成线程安全的List。

List<String> SynchronizedList = Collections.synchronizedList(arrayList);

使用CopyOnWriteArrayList代替ArrayList

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>()

使用vector代替ArrayList

Vector<String> vector = new Vector<String>()

好的,那么为什么ArrayList是不安全的,详细说下

ArrayList主要的线程不安全主要是如下原因:

  • null值错误(并不是我们add的null)
  • 索引越界异常
  • size与我们add数量不符

详细解释如下:

我们先看下ArrayList 内的add的源码

其中的作用大概运行流程如下

  • ensureCapacityInternal方法判断size+1的空间是否足够,需不需要扩容,需要扩容的话就调用grow方法进行扩容
  • 空间足够,在下标为size的位置设置值
  • 将集合大小+1

那么这样就很好理解上述三个错误

  • 部分值为null:假设此时线程1到达arraylist,现在的下标为9,数组大小为11,无需扩容,cpu让出执行权,此时线程2到达arraylist,现在获取的下标也是9,数组大小为11,无需扩容,此时线程1已经在下标为9的地方set了值,然后size++,此时线程2也在下标为9的地方完成了set(线程1数据丢失),此时线程2页将size++,随后size值为11,此时下标为10的位置就是空,不主动干扰,10的位置就是一直为ull
  • 索引越界异常:在上面的情况中,将数组大小改为10,那么此时线程2的size++就会导致索引越界异常
  • size与我们add数量不符:其实这个每次都会出现,其实size++这个操作并不是原子性的,他的步骤为三大步:拿出size,size+1,覆盖掉原来的size,假设两个线程交汇到size++这个操作上了,假设size=9。此时线程1拿到的是9,size++后为10,线程2拿到的是9,size++后为10,并且同时覆盖,这样就会导致size最终为10,但是我们的实际add的为11.

好的,说一下ArrayList的扩容机制

好的,ArrayList在添加元素时,如果当前元素的个数已经超过了list的容量上限,那么就会触发我们的扩容机制。扩容机制大致流程如下:

  • 计算新的容量,新的容量将会是原容量的1.5倍(JDK10之前的版本),然后检查是否超过最大容量限制。
  • 创建新的数组:根据计算的容量创建新的数组
  • 将旧数组的元素复制到新数组中
  • 更新引用,将旧数组的指针指向新的数组
  • 完成扩容

ArrayList的扩容涉及到内存的重新分配和数据的复制,所以需要大量数据时,尽量一次性指定好数组的大小

线程安全的List:CopyOnWriteArrayList是如何实现线程安全的

        这个的底层也是通过一个数组来存储数据的,使用avolatile关键字修饰数组,保证当前线程对数组重新赋值后,其他线程可以感知到到。

    volatile 是一个关键字,用于修饰变量,表示该变量的值可能会被多个线程并发访问和修改。它的主要作用是确保变量的可见性禁止指令重排序,但并不保证操作的原子性。

        写入操作时,加了一把互斥锁保证线程安全

        在写入新元素时,首先会将原来的数组拷贝一份,并且让原来数组的长度+1后得到了一个新的数组,新数组的元素和旧数组一致并多一个长度,然后将新的数据放在最后一个位置,用新数组替换掉老数组即可。

        在我们执行替换操作之前,读取是老数组的数据,老数组并没有加锁,可以直接访问。


http://www.mrgr.cn/news/93483.html

相关文章:

  • leetcode15 三数之和
  • 嵌入式学习第二十三天--网络及TCP
  • 数据开发岗位: 面试测试题(2025年)
  • Android车机DIY开发之软件篇(二十)立创泰山派android编译
  • upload-labs详解(1-12)文件上传分析
  • 【人工智能】Open WebUI+ollama+deepSeek-r1 本地部署大模型与知识库
  • Linux和gcc/g++常用命令总结
  • 【全球化2.0 | ZStack发布Zaku容器云海外版 加速亚太生态布局
  • Java面试第八山!《Spring框架》
  • BUUCTF逆向刷题笔记(1-12)
  • 【零基础C语言】第五节 函数
  • Android实现漂亮的波纹动画
  • 【OMCI实践】wireshark解析脚本omci.lua文件(独家分享)
  • 前端实现版本更新自动检测✅
  • DeepSeek V3 源码:从入门到放弃!
  • 现代密码学体系架构设计原则与实践:基于Python的实现与GPU加速GUI演示
  • 【docker】安装mysql,修改端口号并重启,root改密
  • Anolis服务器Arm64架构服务器配置(其他版本服务器解决方式思路一质)
  • JDK ZOOKEEPER KAFKA安装
  • Vue 系列之:组件通讯