简单阅读一下ArrayList的部分常用方法源码
ArrayList 的优缺点
ArrayList的优点如下:
- ArrayList 底层以数组实现,其内存空间在物理上是连续的,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,按位置读取元素的平均时间复杂度为 O(1)。
- ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
- 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
- 插入元素的时候,也需要做一次元素复制操作。
总结起来ArrayList 新增元素和根据下标获取元素很快,但是插入元素和删除元素很慢,比较适合顺序添加、随机访问的场景。
ArrayList源码解析
域
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;//默认容量
/*
*下面这两个空数组是用来区分不同情况下ArrayList为空时应该怎么操作,
*具体在代码中会解释
*/
private static final Object[] EMPTY_ELEMENTDATA = {};//用于空实例的共享空数组实例。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,以了解在添加第一个元素时应该膨胀多少。
transient Object[] elementData; // 这是用于存储元素的核心数组,非私有以简化嵌套类访问
private int size;
从上面可以看到,ArrayList使用一个Object[]数组elementData来存储元素,并且有一个final的默认容量大小DEFAULT_CAPACITY = 10,说明如果使用无参构造器去创建ArrayList,默认大小就是10。但是这里有个需要注意的点,Java1.7以后,在一开始创建的时候elementData并不会直接被初始化为10容量,而是在第一次调用add()方法时才会初始化,这一点我们在后面会详细讲解。
构造器
我们看一下ArrayList的三个构造器:
//指定初始化容量的构造器
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];//直接根据指定容量初始化数组
} else if (initialCapacity == 0) {
//如果需要初始化一个容量为0的ArrayList,就让elementData 指向EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
//调用无参构造器会让elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
可以看到,上面的构造器中使用到了之前说的两个空数组。在调用无参构造器时,因为没有声明容量,所以让elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而当声明容量为0或者传入的Collection的大小为0时,就让elementData 指向EMPTY_ELEMENTDATA,这样后面就能根据elementData 指向谁来判断是哪种情况,然后进行扩容的处理。
常用方法
add(E e):
//顺序新增元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); //保证容量允许新增元素需不需要扩容
elementData[size++] = e;
return true;
}
可以看到,顺序新增元素的add()方法极其简单,通过ensureCapacityInternal()方法先对数组进行容量保证,如果发现容量不够就会启动扩容机制。
下面我们研究一下ArrayList的扩容机制:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//用于计算需要的容量大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,说明elementData 的容量尚未初始化
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//这说明扩容会影响ArrayList的结构
if (minCapacity - elementData.length > 0)//判断是否需要扩容
grow(minCapacity);
}
//真正的扩容操作
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//扩容50%
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//通过复制操作将元素复制到一个新数组中去
elementData = Arrays.copyOf(elementData, newCapacity);
}
代码比较简单,看注释应该就明白了,所以这里不进行解析了。看客可以结合具体的情况进行理解,比如第一次调用add()、不需要扩容、需要扩容这三种情况。
add(int index,E element):
//插入
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
//复制数组
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
可以看到,ArrayList的插入操作是先将插入点之后的元素往后移一位,然后更新插入点的元素。所以插入操作的快慢主要是由插入点到数组尾部的距离决定的,时间复杂度为O(n)。
set():
public E set(int index, E element) {
rangeCheck(index);//判断下标是否正常
E oldValue = elementData(index);//获取旧元素
elementData[index] = element;//更新
return oldValue;//返回旧元素
}
private void rangeCheck(int index) {
//如果下标越界就抛出异常
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
get():
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
remove():
//根据下标删除
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 方便GC
return oldValue;
}
//指定元素删除
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) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 方便GC
}
逻辑都比较简单,删除元素同样需要复制数组,时间复杂度为O(n)。
关于ArrayList的一些常见问题
ArrayList如何遍历
常见的遍历方法有三种,for、foreach和iterator,那么这三种有什么区别呢?
- for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
- 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
- foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
因为ArrayList实现了 RandomAccess 接口,最佳实践是使用普通的for去遍历。
ArrayList如何边遍历边删除
边遍历边修改 ArrayList (或者说所有集合) 的唯一正确方式是使用 Iterator.remove() 方法,如下:
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
一种最常见的错误代码如下:
for(Integer i : list){
list.remove(i)
}
运行以上错误代码会报 ConcurrentModificationException 异常。集合的iterator在实现时都会根据modCount属性是否为expectedmodCount值去检查集合是否被修改。我们从之前ArrayList的源代码可以看到,add()、remove()都会有一句:modCount++。当使用 foreach语句时,会自动生成一个iterator 来遍历该 ArrayList,但同时该 ArrayList执行了 remove() 导致modCount的数值变化,那么就会抛出异常。
下面是ArrayList的iterator模式实现的部分源码,可以对照着理解一下:
public E next() {
checkForComodification();//检查ArrayList结构是否被修改
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//可以安全地再遍历中删除元素
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();//检查ArrayList结构是否被修改
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)//如果modCount不符合预期就抛出错误
throw new ConcurrentModificationException();
}
为什么 ArrayList 的 elementData 加上 transient 修饰?
ArrayList 中的数组定义如下:
transient Object[] elementData;
再看一下 ArrayList 的定义:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,并且重写了 writeObject 实现:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。
多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
当然也可以直接使用Vector,两者内部都是通过synchronized来保证线程安全的。