1. java面试基础三:集合框架List面试题
1.1. java集合框架里面List常见面试题
- 说一下Vector和ArrayList,LinkedList的联系和区别?分别使用场景
从两点回答:
1、线程安全
ArrayList:底层是数组实现,线程不安全,好处就是查询和修改非常快,但是增加和删除慢。
LInkedList:底层是双向链表,查询和修改速度慢,但是增加和删除速度快。
Vector:底层也是数组实现,线程安全的使用了Synchronized加锁。
2、使用场景
Vector: 已经很少用了,
LinkedList:增加和删除多用LinkedList。
ArrayList:查询和修改多,用ArrayList.
- 如果要保证线程安全ArrayList应该如何做?
方案一:自己写个包装类,根据业务一般是add/update/remove加锁。
方案二:用Collections.synchronizedList(new ArrayList<>());进行加锁。
方案三:CopyOnWriteArrayList<>() 使用 ReentrantLock加锁,思想是:先获取原先的数组,获取后通过ReentrantLock进行加锁,然后把原来的数组copy到一个新的数组中,再将新增的数组元素添加进去,然后再去掉锁,然后再将原来数组的地址指向新数组,再返回回去。
- CopyOnWriteArrayList和SynchronizedList实现线程安全有什么区别?使用场景是怎样的?
CopyOnWriteArrayList:执行修改操作,会拷贝一份新的数据,(add/set/remove).代价昂贵,修改好后,会将原来的集合指向新的集合来完成操作,使用ReentrantLock来保证不会有多个线程同时修改。
使用场景:适合读操作远远大于写操作的场景,读操作是不需要加锁的,直接获取,但是删除和增加需要加锁,读多写少。
Collections.synchronizedList:线程安全的原因就是几乎每个方法都是使用synchronized来同步加锁的,
使用场景:写操作性能比CopyOnWriteArrayList好,但是读操作性能没有CopyOnWriteArrayList好。
- CopyOnWriteArrayList的设计思想是怎样的?有什么缺点?
设计思想:读写分离+最终一致,
缺点:占内存,由于写时复制,内存里面会存在两个对象占用内存,如果对象大则容易发生YongGC和FullGC。
1.2. LIst的扩容机制
- 说一下ArrayList的扩容机制
1、这个分版本,jdk1.7之前ArrayList的默认大小是10,jdk1.7之后是0
2、未指定集合容量,默认是0,如果已经指定大小则集合大小为指定大小。
3、当集合new出来时,容量为0,当第一次添加元素时,集合扩容为10
4、当集合元素大于10时,扩容容量的大小=原始大小+原始大小/2
- 手写一个简单版的ArrayList(包含构造函数(有参和无参,add(obj) 、扩容机制))
import java.io.Serializable;
public class MyArrayList implements Serializable{
//使用这个字段,来判断当前集合类是否被并发修改,即迭代器并发修改的fail-fast机制。
private transient int modCount=0;
//第一次扩容容量
private static final int DEFAULT_CAPACITY=10;
//用于初始化空的list
private static final Object[] EMPTY_ELEMENT_DATA= {};
//实际存储的元素
transient Object[] elementData;//transient避免被序列化
//实际list集合大小,从0开始
private int size;
//构造方法
public MyArrayList() {
this.elementData= EMPTY_ELEMENT_DATA;
}
public MyArrayList(int initialCapcity) {
if(initialCapcity>0) {
this.elementData=new Object[initialCapcity];
}else if(initialCapcity==0) {
this.elementData=EMPTY_ELEMENT_DATA;
}else {
throw new IllegalAccessError("参数异常");
}
}
public boolean add(Object e) {
//判断容量
ensureCapacityInternal(size+1);
//使用下标复制,尾部插入,这个是下标,还有一个容量。
elementData[size++]=e;
return true;
}
/**
* 计算容量,确保容量
* @Title: ensureCapacityInternal
* @Description:
* @param minCapacity
* @author:kaifan·Zhang
* @date 2020年3月11日
* @version 1.0
*/
private void ensureCapacityInternal(int minCapacity) {
//用于并发判断
modCount++;
//如果是初次扩容,则使用默认容量。
if(elementData==EMPTY_ELEMENT_DATA) {
minCapacity=Math.max(DEFAULT_CAPACITY, minCapacity);
}
//是否需要扩容,需求的最少容量,大于现在数组的长度,则要扩容
if(minCapacity-elementData.length>0) {
int oldCapacity=elementData.length;
int newCapacity=oldCapacity+(oldCapacity>>1);
//如果新容量<最小容量,将最小
if(newCapacity-minCapacity<0) {
newCapacity =minCapacity;
}
//创建新数组
Object[] objects=new Object[newCapacity];
//将旧的数据cop到新的数组里面。
System.arraycopy(elementData, 0, objects, 0, elementData.length);
//修改引用
elementData=objects;
}
}
/**
* 通过下标获取对象。
* @Title: get
* @Description:
* @param index
* @return
* @author:kaifan·Zhang
* @date 2020年3月11日
* @version 1.0
*/
public Object get(int index) {
rangeCheck(index);
return elementData[index];
}
private void rangeCheck(int index) {
if(index>size||size<0) {
throw new IndexOutOfBoundsException("数据越界");
}
}
/**
* 判断对象所在的位置
* @Title: indexOf
* @Description:
* @param o
* @return
* @author:kaifan·Zhang
* @date 2020年3月11日
* @version 1.0
*/
public int indexOf(Object o) {
if(o==null) {
for(int i=0;i<size;i++) {
if(elementData[i]==null) {
return i;
}
}
}else {
for(int i=0;i<size;i++) {
if(o.equals(elementData[i])) {
return i;
}
}
}
return -1;
}
/**
* 修改下标对应的值
* @Title: set
* @Description:
* @param index
* @param obj
* @return
* @author:kaifan·Zhang
* @date 2020年3月11日
* @version 1.0
*/
public Object set(int index,Object obj) {
rangeCheck(index);
Object oldValue=elementData[index];
elementData[index]=obj;
return oldValue;
}
/**
* 根据索引删除下标对应的元素
* @Title: remove
* @Description:
* @param index
* @return
* @author:kaifan·Zhang
* @date 2020年3月11日
* @version 1.0
*/
public Object remove(int index) {
rangeCheck(index);
//用于并发判断,当迭代器操作时,会有很多操作,
//当操作到最后会与刚进入方法时的modCount进行比较,如果不同,就会抛出异常
modCount++;
Object oldValue =elementData[index];
//计算要删除的位置后面还有几个元素
int numMoved=size-index-1;
if(numMoved>0) {
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
//将多出的位置设置为null,没有引用对象。垃圾收集器就可以回收。如果不置空,将会保存一个引用
//可能会造成内存泄露。
elementData[--size]=null;
return oldValue;
}
/**
* 获取数组实际大小
* @Title: size
* @Description:
* @return
* @author:kaifan·Zhang
* @date 2020年3月11日
* @version 1.0
*/
public int size() {
return this.size;
}
}