ArrayList源码分析
ArrayList类的继承和接口
首先看一下ArrayList这个类的继承和实现关系
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
从上面可以看出它继承了AbstractList,实现了RandomAccess, Cloneable, java.io.Serializable这几个接口
那么来分析一下这几个的作用吧
AbstractList
通过看这个抽象类的源码可以知道,这个就是ArrayList的骨架,所以ArrayList会通过AbstractList来构建自己的骨架和方法
RandomAccess
这个接口提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。在实践中,如果集合实现了这个接口,那么就建议采取随机访问,这样速度会更快,如果没有实现这个接口,建议采取顺序访问;因为有无这个接口然后选择访问的方式会对访问速度有很大的影响。
Cloneable
这个接口主要是提供了ArrayList可以被拷贝的功能java.io.Serializable
这个接口主要提供了ArrayList的序列化功能,因为实现了序列化,所以在实践中,如果有大量数据需要进行存储或者取出,那么就可以考虑使用ArrayList集合进行操作。
ArrayList是如何初始化的?
先声明接下来会出现的参数
//初始化容量为10
private static final int DEFAULT_CAPACITY = 10;
//定义一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//定义一个默认容量为空的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//定义一个数组(这个也就是ArrayList底层使用的数组)
transient Object[] elementData; // non-private to simplify nested class access
//ArrayList中元素所占数组的大小
private int size;
然后从它的三个构造方法说起:
ArrayList list1 = new ArrayList<>();
ArrayList list2 = new ArrayList<>(12);
ArrayList list3= new ArrayList<>(list1);
第一个无参构造
进行debug来看看怎么初始化的
//可以看到,无参构造初始化,进入的就是他自身的无参构造器 public ArrayList() { //这是将本身的数组设置为空数组 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
第二个有参构造
//有参构造就会进入到这个构造方法中 //initialCapacity这个就是传过来的初始化容量参数 public ArrayList(int initialCapacity) { //判断如果初始化容量的参数大于0,那么就将新创建的数组(这个数组的容量就是前面传过来的初始化容量大小)赋给本地的数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //如果初始化容量大小等于零,那么就将本地数组置为空数组 this.elementData = EMPTY_ELEMENTDATA; } else { //如果上面两种条件都不满足,那么就会报出非法容量异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
第三个有参构造
//进入到这个构造方法中,参数传递是集合 public ArrayList(Collection<? extends E> c) { //首先将传过来的集合变成数组,然后将这个数组赋给本地数组 elementData = c.toArray();//有关这个方法的调用在下面代码中指出 //这里将size赋值,然后判断本地数组的长度 是否等于0 if ((size = elementData.length) != 0) { // 如果不等于0,再进行判断本地数组的类和对象数组的类是否相同 if (elementData.getClass() != Object[].class) //如果不相同,那么就会将本地数组的类型转换成对象数组类型 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 如果上述条件都不符合,那么就会将本地数组置为空 this.elementData = EMPTY_ELEMENTDATA; } } //这是上面的toArray方法 public Object[] toArray() { //而这个方法的底层实现是将数据拷贝到一个新数组,然后将这个新数组进行返回 return Arrays.copyOf(elementData, size); } //original就是传过来的数据,newLength就是传过来的集合长度 public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } //实际上调用的就是这个方法!!!!! public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") //这里进行三元运算判断,判断新类型是否和对象数组类型相同,这里不管判断是正确还是不正确,都会创建一个新数组,然后把数据放到这个新数组里面,进行返回 T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); //这里是进行数据拷贝,Math.min(original.length, newLength)这个是比较传过来的数据长度和传过来的长度大小。 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
通过上面的构造方法,可以知道ArrayList是如何初始化的
ArrayList是怎么实现增删改查的?
下面再通过它的增删改查来进一步探索
从增加开始说起,直接上代码
//就从这个代码开始debug吧
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
//首先进入的是这个添加方法中,这是boolean类型的方法,但是它返回的永远是ture
public boolean add(E e) {
//这个方法主要是判断是否是第一次添加,然后数组是否初始化过,然后再进行后面的扩容判断。
ensureCapacityInternal(size + 1); // Increments modCount!!
//在将元素添加进数组之前,需要判断这个数组的容量是否能够让这个元素添加进去
//将size作为下标,存储传进来的元素
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//因为是第一次添加操作,所以传过来的minCapacity最小值是1,也就是前面的size + 1
//这里进行判断,本地数组是否为空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果是空数组,那么就将这个最小容量设置为默认容量,也就是10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//进行扩容判断
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//计数用的
modCount++;
//判断预扩展的值与当前数组的容量大小
if (minCapacity - elementData.length > 0)
//如果大于,说明数组容量不够,会进行扩容操作
grow(minCapacity);
}
private void grow(int minCapacity) {
// oldCapacity将本地数组长度记录下来
int oldCapacity = elementData.length;
//>>是右移,也就是除以2的几次幂
//<<是左移,也就是乘以2的几次幂
//newCapacity=旧的容量+将旧的容量右移后的结果
//得到的结果就是newCapacity是原来旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断如果新的容量减去最小容量小于0
if (newCapacity - minCapacity < 0)
//那么新容量就还是等于最小容量(意味着没有扩容)
newCapacity = minCapacity;
//如果新的容量大于最大数组容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
//那么就会
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
//如果最小容量小于0,内存溢出异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果上述条件不满足,进行三元运算,如果最小容量大于最大数组容量,那么就会将这个更大的值返回,否则就返回最大数组容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
将思路捋清:这个代码主要是在添加元素之前将确保一下数组容量的大小,以便后面可以正常的元素插入。在这个确保容量的时候,首先如果是第一次添加进来的,那么因为我们初始化集合是用的无参构造方法,所以先将这个数组的大小设置为默认大小。设置完之后,再判断添加的元素之后的长度是否大于当前数组的容量,如果大于那么就会进入到扩容中,然后将旧的容量扩充为原来的1.5倍(这里主要是针对第一次进来时的情况:如果扩充的容量比最小容量还小,那么就标志这扩容失败,容量还是采用最小容量,如果容量大于最大数组容量那么就会再进行相关的判断….)
思考思考思考!!!!!
到了这里,其实我还是有很多的疑问,虽然源码看明白了,但是为什么是这么个流程呢?
带着问题,将源码更通透的理解一下:
为什么是否扩容的判断条件如此难懂?
minCapacity - elementData.length > 0 ????
为什么最小容量大于元素存储的容量还要进行扩容,这里最小容量大于存储容量,那就证明有空间存储啊,为啥还要进行扩容,反而最小容量如果小于那就什么操作也不做????
下面纯属个人理解:
首先这里有一个错误的理解:受第一次添加数据的影响,我把minCapacity当成了一个定值,我一直把这个值当做是10来处理,其实这个最小容量是根据每次添加数据动态更新的值;elementData.length我把它理解成当前数组中存储元素的长度,其实是初始化数组长度的值!!!因此,我无法理解这个扩容的判断条件。
正确思路:将上述的错误思想改正之后,豁然开朗,解决思路就是我来了一个一万次循环将元素添加进集合当中,那么这个时候,你会发现这个minCapacity是个什么东西呢,它其实就是在我们添加元素之前,将数组容量预扩展一个存放元素的空间,然后用这个预扩展的值减去当前数组的长度,如果这个长度大于数组长度,那么就说明数组长度不够,这时候就会调用grow方法,将数组扩容为原来的1.5倍。
如果上面的描述还是不懂,那么现在假设现在循环到了第十一次,到调用这个添加方法的时候,首先判断容量是否够,传过去的参数就是minCapacity=11,而这个时候elementData.length就是当时初始化的长度,也就是10,所以说现在是不是就必须得将数组扩容呢?那么就进入到grow方法当中,一顿操作下来,那容量不就变成了15嘛,然后不就可以继续存元素了吗???
经过上面的一顿分析之后,下面再看看另外的几个添加方法:
public void add(int index, E element) 在指定位置添加元素
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
//添加到指定位置
list1.add(1,"abc");
//进入到这个方法当中
public void add(int index, E element) {
//校验传过来的下标是否合格
rangeCheckForAdd(index);
//校验数组容量是否设置过,然后判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
//src表示源数组,srcPos表示源数组要复制的起始位置,desc表示目标数组,destPos在目标数组中开始赋值的位置,length表示要复制的长度。
//主要就是这段copy代码,这段代码是怎么实现的呢???
//首先数据源是elementData{aaa,bbb,ccc},也就是原数组,要复制的起始位置就是index=1(bbb),目标数组还是elementData{aaa,bbb,ccc},开始赋值的位置就是2(ccc)开始,要复制的长度为3-1=2;那么根据上述条件,我们知道复制完之后数组的数据变成了elementData{aaa,bbb,bbb,ccc}
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//然后将索引的位置变成要添加的元素值(在指定的地方添加元素)
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
//是否大于数组长度,是否小于0,如果是就会抛出索引越界异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public boolean addAll(Collection<? extends E> c) 添加集合元素
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
ArrayList list3= new ArrayList<>(list1);
list3.addAll(list1);
//进入到这个方法中来
public boolean addAll(Collection<? extends E> c) {
//首先将集合变成一个对象数组
Object[] a = c.toArray();
//记录数组的长度
int numNew = a.length;
//校验数组的容量,判断是否扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//又来到的熟悉的地方,同样的,我们根据上一个方法来进行分析
//数据源:a{aaa,bbb,ccc} 复制起始位置0 目标数组elementData{} 目标数组的位置0 复制的长度3
//那么最终得到的结果就是复制过来的数组elementData{aaa,bbb,ccc}
System.arraycopy(a, 0, elementData, size, numNew);
//数组的长度变成原本的长度加上新添加数据的长度
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) 将集合添加到指定位置上
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
ArrayList list3= new ArrayList<>();
list3.add("ddd");
list3.add("eee");
list3.addAll(1,list1);
//进入到这个方法中
public boolean addAll(int index, Collection<? extends E> c) {
//判断下标是否越界
rangeCheckForAdd(index);
//将集合先变成对象数组
Object[] a = c.toArray();
//将数组的长度取出
int numNew = a.length;
//校验是否有初始容量,以及是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//记录要移动的步数
int numMoved = size - index;
//如果大于0
if (numMoved > 0)
//首先是进行第一次拷贝,这次拷贝主要是将目标数组扩充为可以容纳新数组的数组
//数据源elementData{ddd,eee} 复制的起始位置1 目标数组elementData 在目标数组赋值的位置1+3 复制的长度1 复制完成之后的数组变成elementData{ddd,eee,null,null,eee}
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//第二次拷贝才是将真正的数据添加进数组中
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
修改方法set
上代码
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
list1.set(1,"ddd");
public E set(int index, E element) {
//判断索引下标是否越界
rangeCheck(index);
//将旧的值取出
E oldValue = elementData(index);
//将新的值修改
elementData[index] = element;
//返回旧的值
return oldValue;
}
获取方法get
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
list1.get(2);
public E get(int index) {
//判断索引下标是否越界
rangeCheck(index);
//返回当前索引的值
return elementData(index);
}
toString方法源码解析
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
list1.toString();
//首先得明确一点,这个方法不是ArrayList的而是AbstractList中的方法
public String toString() {
//主要使用的迭代器
Iterator<E> it = iterator();
//判断如果为空,就直接返回空串
if (! it.hasNext())
return "[]";
//如果不为空,那么用StringBuilder进行字符串拼接
StringBuilder sb = new StringBuilder();
//先将结构搭建好
sb.append('[');
//然后用循环将里面的数据一一拼接到字符串上
for (;;) {
E e = it.next();
//这里通过三元运算,判断y
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
//如果没有数据了,就进行结尾
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
迭代器的源码
这里我会通过三个不同的案例,来分析一下迭代器具体的源码,然后引申出相关的问题
第一个例子:通过迭代器遍历,使用集合自带的方法删除目标元素
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
Iterator it = list1.iterator();
while(it.hasNext()){
String s = (String) it.next();
if (s.equals("aaa")){
list1.remove(s);
}
}
将这个代码运行之后,你会发现代码报错
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at jihe.test.main(test.java:17)
为什么会报错呢???带着问题,查看源码!!!
//先进入这个迭代器的方法中,而后会发现,一个内部类继承iterator重写了迭代器方法
public Iterator<E> iterator() {
return new Itr();
}
//主要来看一下这个类的具体实现
private class Itr implements Iterator<E> {
int cursor; // 这里是光标位置
int lastRet = -1; // 返回最后一个元素的索引
//将实际修改次数赋值给预期修改次数(报错的关键语句)
int expectedModCount = modCount;
//判断光标位置是否到达末尾
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//检查实际修改次数和预期修改次数是否一样,如果不一样那么就会报出刚刚的错误ConcurrentModificationException(报错的关键方法)
checkForComodification();
//将光标的值赋值给i
int i = cursor;
//size:就是数组的长度
//然后用i和size进行比较,如果光标的值大于size的值,那么就会抛出异常NoSuchElementException
if (i >= size)
throw new NoSuchElementException();
//将集合数组赋值给elementData
Object[] elementData = ArrayList.this.elementData;
//如果光标的值大于数组的长度抛出异常ConcurrentModificationException
if (i >= elementData.length)
throw new ConcurrentModificationException();
//光标自增
cursor = i + 1;
//将i的值赋值给lastRet,并且将当前遍历到的数据进行返回
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
//判断实际修改次数和语气修改次数是否相等
if (modCount != expectedModCount)
//不想等就直接报出ConcurrentModificationException异常
throw new ConcurrentModificationException();
}
//上面的遍历大致就是那么个思路,然后根据程序的执行,我们会进行比较判断,如果当前遍历的值与要删除的值相同,那么就调用ArrayList自带的移除方法
public boolean remove(Object o) {
//首先判断传过来的这个值是否为空
if (o == null) {
//如果为空,那么就会依次遍历,将数组中的空值移除
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//如果不为空,那么就会进行遍历
for (int index = 0; index < size; index++)
//找到与传过来的值相匹配的元素地址,然后进行移除操作
if (o.equals(elementData[index])) {
//移除
fastRemove(index);
return true;
}
}
return false;
}
//在这个移除操作中
private void fastRemove(int index) {
//首先会把实际修改次数加1
modCount++;
//然后计算移除的位置
int numMoved = size - index - 1;
if (numMoved > 0)
//将移除元素后面的元素进行拷贝
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将元素置为空
elementData[--size] = null; // clear to let GC do its work
}
那么通过上面的案例,可能还是没有明白为什么会报错呢,程序都执行到了结尾了,数据也移除了,那么为什么还会报错呢??
请继续往下看,程序确实已经完成了移除操作,但是,程序还没有执行完,继续往下走,会再次进入迭代器的遍历当中,当遍历了一个元素之后,再次调用next方法,就可以看到next方法中首先出现的checkForComodification();,那么核心就在这个方法当中了,还记得之前在添加数据的时候,modCount会记录修改的次数,之前添加了三个元素,也就是modCount=3,然后将modCount的值赋给了expectedModCount,那么expectedModCount的值是不是也等于3,但是在刚刚移除元素的过程中,是不是有modCount++出现,那么这时候modCount是不是等于4,这样是不是一切都明了了,此时modCount != expectedModCount成立,就会报出刚刚的错误了。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
第二个例子:和第一次是一样的代码,注意不同的是这次移除的是倒数第二个元素
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
Iterator it = list1.iterator();
while(it.hasNext()){
String s = (String) it.next();
if (s.equals("bbb")){
list1.remove(s);
}
}
前面的操作流程都是一样的,但是这次运行结果竟然不报错!!!!??
为什么不报错呢???
这属于是特殊情况,为什么这么说呢,根据刚刚的代码,再来分析一下思路,如果删除的是倒数第二个元素,那么在下一次迭代器遍历中,首先看到的判断条件是不是public boolean hasNext(){return cursor != size;}
,实际上就是这里出了问题,现在想一下,集合中一共有三个元素,在进行了移除操作,就剩下两个元素,而且在移除操作最后一步elementData[–size] = null,首先是将size进行–操作,这样就说明此时的size大小为2,而光标在上一次的遍历中是不是cursor = i + 1有这个操作,这时候cursor是不是也变成了2,那么hasNext这个判断条件就变成了false,既然变成了false,就会直接跳出遍历,程序并没有报错!!
第三个例子:和第一次一样的代码,不同的是这一次使用的是迭代器自带的移除方法
ArrayList list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
Iterator it = list1.iterator();
while(it.hasNext()){
String s = (String) it.next();
if (s.equals("ccc")){
//注意,这里是迭代器自带的方法进行元素的删除
it.remove();
}
}
前面遍历操作的流程大致都是一样的,主要看一下这个迭代器自带的删除方法有什么异同
下面进行源码分析
public void remove() {
//来看一下lastRet是什么?
//之前在前面的代码中,是不是在遍历的next方法中出现,这个lastRet指向的就是,当前遍历的元素,也可以说是遍历到的最后一个元素(不是遍历完,而是遍历到当前的元素,当前元素的下表作为lastRet的值)。这个lastRet的值就是需要remove的那个元素的下标
//首先判断这个下标是否小于0,如果是就会抛出IllegalStateException
if (lastRet < 0)
throw new IllegalStateException();
//检查预期修改次数和实际修改次数是否相同
checkForComodification();
try {
//调用集合本地的remove方法,将lastRet这个下标指向的值删除
ArrayList.this.remove(lastRet);
//将当前遍历的最后一个值得下标赋值给光标
cursor = lastRet;
//将lastRet置为-1
lastRet = -1;
//将实际修改的次数赋值给预期修改次数
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public E remove(int index) {
//校验下表是否越界
rangeCheck(index);
//实际修改次数加1
modCount++;
//记录旧值
E oldValue = elementData(index);
//要移动的步数
int numMoved = size - index - 1;
if (numMoved > 0)
//进行数组拷贝
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将指定的值移除
elementData[--size] = null; // clear to let GC do its work
//将旧值返回
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
//如果索引下标大于集合大小,就会报错
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
总结:可以看到,通过迭代器的方法进行删除的时候,大致思路是一样的,而且底层的删除还是用的ArrayList自带的集合进行删除的,不过不同的是,迭代器的删除方法中**expectedModCount = modCount;**多出了这样的一个步骤,所以迭代器删除是不会报错的。
clear、contains、isEmpty源码
clear方法:
//简单明了 public void clear() { //实际修改次数自增 modCount++; // 通过遍历,将所有的值置为空,以便垃圾回收机制将它们进行回收 for (int i = 0; i < size; i++) elementData[i] = null; //将数组的大小也置为0 size = 0; }
IsEmpty方法:
//判断数组大小是否为0 public boolean isEmpty() { return size == 0; }
contains方法:
ArrayList list1 = new ArrayList<>(); list1.add("aaa"); list1.add("bbb"); list1.add("ccc"); list1.contains("abc");
//首先进入这个方法中,返回indexOf(o) >= 0比值是否大于0,如果大于就是包含相应的值,如果小于0就是不包含 public boolean contains(Object o) { return indexOf(o) >= 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; } //如果都没有找到,就返回-1 return -1; }
最终可以通过判断**list1.contains(“abc”);**的返回值是ture还是false来判断是否包含所传入的值。
考虑以下几个问题
ArrayList是如何扩容的?
ArrayList频繁扩容导致添加性能几句下降,如何处理?
只想到了使用初始容量的构造方法,不过浪费空间ArrayList插入或者删除元素一定比LinkedList慢吗?
不一定,这个就要看是否针对某个位置进行相应的插入和删除操作,如果不针对位置进行插入和删除,那么肯定是LinkedList比较快的,如果针对位置,那么它们俩的性能是差不多的。ArrayList是线程安全的吗?
不是线程安全的,那么又如果解决这个不安全,或者手写一个不安全的案例看看?
如何解决线程不安全?
使用Vector、使用synchronize、以及Collections.synchronizedList()、lock()
如何将某个ArrayList复制到另一个ArrayList中
构造方法、clone、添加多线程下保证正常读写?
copyOnWriteArrayListArrayList和LinkedList的区别?