🌍List常见基础面试题
1.说下Vector和ArrayList、LinkedList联系和区别?分别的使用场景
- 线程安全
- ArrayList:底层是数组实现,线程不安全,查询和修改非常快,但是增加和删除慢
- LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,但是增加和删除速度快
- Vector: 底层是数组实现,线程安全的,操作的时候使用synchronized进行加锁
- 使用场景
- Vector已经很少用了
- 增加和删除场景多则用LinkedList
- 查询和修改多则用ArrayList
2.如果需要保证线程安全,ArrayList应该怎么做,用有几种方式
方式一:自己写个包装类,根据业务一般是add/update/remove加锁
方式二:Collections.synchronizedList(new ArrayList<>()); 使用synchronized加锁
方式三:CopyOnWriteArrayList<>() 使用ReentrantLock加锁
3. 如果回答到上面的点则继续问,了解CopyOnWriteArrayList吗?Collections.synchronizedList实现线程安全有什么区别, 使用场景是怎样的?
-
CopyOnWriteArrayList:执行修改操作时,会拷贝一份新的数组进行操作(add、set、remove等),代价十分昂贵,在执行完修改后将原来集合指向新的集合来完成修改操作,源码里面用ReentrantLock可重入锁来保证不会有多个线程同时拷贝一份数组
-
场景:读高性能,适用读操作远远大于写操作的场景中使用(读的时候是不需要加锁的,直接获取,删除和增加是需要加锁的, 读多写少)
-
Collections.synchronizedList:线程安全的原因是因为它几乎在每个方法中都使用了synchronized同步锁
- 场景:写操作性能比CopyOnWriteArrayList好,读操作性能并不如CopyOnWriteArrayList
-
4.CopyOnWriteArrayList的设计思想是怎样的,有什么缺点?
答案:设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象大则容易发生Yong GC和Full GC
5.说下ArrayList的扩容机制是怎样的
注意:JDK1.7之前ArrayList默认大小是10,JDk1.7之后是0
未指定集合容量,默认是0,若已经指定大小则集合大小为指定的;
当集合第一次添加元素的时候,集合大小扩容为10
ArrayList的元素个数大于其容量,扩容的大小= 原始大小+原始大小/2
6.设计一个简单的ArrayList【需要包含 构造函数(有参和无参)、add(obj)、 扩容机制】
package com.cx330;
import java.io.Serializable;
public class MyArrayList implements Serializable {
/**
* 第一次扩容的容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于初始化空的List
*/
private static final Object[] EMPTY_ELEMENT_DATA = {};
/**
* 实际存储的元素
* transient:防止被序列化
*/
transient Object[] elementData;
/**
* 实际List存储元素的长度
*/
private int size;
public MyArrayList() {
this.elementData = EMPTY_ELEMENT_DATA;
}
public MyArrayList(int initCapacity) {
if (initCapacity > 0) {
this.elementData = new Object[initCapacity];
} else if (initCapacity == 0) {
this.elementData = EMPTY_ELEMENT_DATA;
} else {
throw new IllegalArgumentException("非法参数! 数组长度应该大于0");
}
}
/**
* 添加元素
* @param e
* @return
*/
public boolean add(Object e) {
//判断容量
ensureCapacityInterval(this.size+1);
//尾部插入
this.elementData[size++]=e;
return true;
}
/**
* 检测是否扩容
* @param minCapacity
*/
private void ensureCapacityInterval(int minCapacity) {
//初次扩容
if (this.elementData==EMPTY_ELEMENT_DATA) {
minCapacity=Math.max(DEFAULT_CAPACITY,minCapacity);
}
//需要的最容量大于当前数组的长度,则扩容
if (this.elementData.length<minCapacity) {
int oldCapacity=this.elementData.length;
int newCapacity=oldCapacity+(oldCapacity>>>1);
//扩容完之后新的容量还小于需要扩容的容量
if (newCapacity-minCapacity<0) {
newCapacity=minCapacity;
}
Object[] objects=new Object[newCapacity];
System.arraycopy(this.elementData,0,objects,0,this.elementData.length);
this.elementData=objects;
}
}
/**
* 通过下标获取对象
* @param index
* @return
*/
public Object get(int index){
//检查下表是否合法
rangeCheck(index);
return this.elementData[index];
}
private void rangeCheck(int index) {
if (index>=this.size||index<0) {
throw new IllegalArgumentException("非法下标");
}
}
/**
* 通过对象获取下标
* @param e
* @return
*/
public int indexOf(Object e){
if (e==null) {
for (int i = 0; i < this.elementData.length; i++) {
if (this.elementData[i]==null) {
return i;
}
}
}else {
for (int i = 0; i < this.elementData.length; i++) {
if (e.equals(this.elementData[i])) {
return i;
}
}
}
return -1;
}
/**
* 更新指定下标的元素
* @param index
* @param e
* @return
*/
public Object set(int index,Object e){
rangeCheck(index);
Object oldE=elementData[index];
elementData[index]=e;
return oldE;
}
/**
* 删除指定下标的元素
* @param index
* @return
*/
public Object remove(int index){
rangeCheck(index);
Object e=elementData[index];
//判断删除元素的后面还有多少个元素
int moveSize=size-index-1;
if (moveSize>0) {
System.arraycopy(elementData,index+1,elementData,index,moveSize);
}
//将多出的位置置为空,进而没有对象的引用,垃圾对象可以对其进行回收,如果不为空,将会有一个对象的引用,可能造成内存泄露
elementData[--size]=null;
return e;
}
/**
* 获取数组实际大小
* @return
*/
public int size(){
return this.size;
}
}
补充:这一题如果要考虑并发修改这个集合的话,我们可以定义一个int类型的变量 modCount 在add,set,remove之前保存这个值,在进行写操作之后再次获取这个值,如果前后两次不相同,那么就说明存在多线程修改,我们就可以抛异常。