什么是ArrayList
ArrayList的底层是基于一个数组实现的,实际上ArrayList就是一个动态数组。
ArrayList的一些特性
- ArrayList并不是线性安全的,在多线程环境下不能使用。
- 每个ArrayList实例都有一个容量属性,该容量属性是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。
ArrayList是如何实现的
ArrayList实现了List接口,其实其操作就是对于数组的操作。下面我们来通过源代码进行讲解。
一 属性
//使用对象数组存储ArrayList中的元素
private transient Object[] elementData;
//对象数组中包含元素的数量
private int size;
transient关键字是啥意思
对象的属性加上transient关键字后,该对象被序列化时,此属性不会被保存。
二 构造方法
ArrayList提供了三种构造方法,分别可以构造以下三种列表
- 指定容量的空表
- 容量为10的空表
- 包含指定collection元素的列表
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 新建一个数组
this.elementData = new Object[initialCapacity];
}
// ArrayList无参构造函数。默认容量是10。
public ArrayList() {
this(10);
}
// 创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
三 增加元素
用一个新元素来替代旧元素,并返回旧元素
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
添加一个元素至列表尾部
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
添加元素至列表的指定位置
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果数组长度不足,将进行扩容。
ensureCapacity(size+1);
// 将 elementData中从Index位置开始、长度为size-index的元素,
// 拷贝到从下标为index+1位置开始的新的elementData数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
将Collection的所有元素添加至列表尾部
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
将Collection的所有元素添加至列表指定位置
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew);
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
我们知道ArrayList的底层是基于数组实现的,而数组是有长度的,当进行增删改查时,若数组下标没有越界自然一切好说。但,比如像上面的add方法,若增加元素时数组越界了怎么办?ArrayList自然不会允许这种情况发生,其中的精髓便在于ensureCapacity方法,它实现了数组的扩容,这里我们只需对它有一个初始概念即可,下面我们会对其进行源码分析。
查阅元素
返回指定位置的元素
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
删除元素
删除指定位置的元素
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
移除列表中首次出现的指定元素
public boolean remove(Object o) {
// 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 类似remove(int index),移除列表中指定位置上的元素。
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
}
E remove(int index)与void fastRemove(int index)的区别
void fastRemove(int index)与E remove(int index)相比,跳过了判断边界的处理。从上面的boolean remove(Object o)方法可以发现,当我们找到符合条件的对象,自然易得一个数组下标index,而该index是不可能越界的,故使用void fastRemove(int index)对对象进行删除。
下面是fastRemove的代码
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;
}
上面挺多方法的实现都用到了removeRange方法,这个方法的功能是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。下面直接上源码。
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
int newSize = size - (toIndex-fromIndex);
while (size != newSize)
elementData[--size] = null;
}
ArrayList的关键方法ensureCapacity
每次我们向ArrayList添加元素时,都会检查ArrayList是否会发生数组越界。如果发生数组越界,ArrayList会通过void ensureCapacity(int minCapacity)来进行扩容。
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
我们可以发现,当数组进行扩容时,会将老数组的元素拷贝一份放在新数组中,并进行大约1.5倍的容量增长。其实这种代价是相当昂贵的,为此我们可以在ArrayList初始化时便指定一个相当大的容量或手动通过ensureCapacity方法来实现数组扩容。
ArrayList和Vector的区别之处
- ArrayList在内存不够时默认是扩展50% + 1,Vector是默认扩展1倍
- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销
- Vector提供indexOf(obj, start)接口,ArrayList没有
ArrayList的一些其他方法
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
该方法将底层数组的容量调整为当前列表保存的实际元素的大小,在某些特定情况下用于节约空间。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
该方法用于将ArrayList转化为静态数组,拷贝了elementData从0至size-1位置的元素到新数组并返回。
public <T> T[] toArray(T[] a) {
if (a.length < size)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。
在ArrayList的实现中大量使用了Arrays.copyof()和System.arraycopy()方法,下面我们对其进行一定的讲解。
Arrays.copyof()
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) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。
System.arraycopy()
该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
对ArrayList的总结
- 在确定ArrayList的元素数量之后使用ArrayList较好,否则建议使用LinkedList,因为若不确定元素数量可能会不断发生数组扩容,此操作非常耗时。
- ArrayList基于数组实现,可以通过下标索引直接查找元素,但进行增加或删除操作时需要大量移动元素,故查找效率高,增加或删除效率低。
- ArrayList中允许元素为null,故在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理。