ArrayList详解
1、简介
ArrayList是我们比较常用的一个Java集合类,内部是使用Object数组来存储元素,允许存储null元素,在添加元素时,会根据元素的个数来自动增加数组的大小。值得注意的是ArrayList是非线程安全的,可以使用Collections.synchronizedList方法把ArrayList对象转换成线程安全的对象,这个方法实际上是把ArrayList的每一个方法都放到synchronized关键字修饰的语句块中,因此在平时使用中,如果读多写少的话,使用ReentrantReadWriteLock可重入的读写锁来控制对ArrayList的读写,效率会更高一些。
由于ArrayList内部是使用Object数组来存储数据的,而在添加元素时扩容是将数组容量增加到原大小的1.5倍,因此ArrayList的容量是大于或者等于当前元素数量的,在ArrayList内部是使用size属性来记录当前元素数量,扩容有可能会造成一定的空间浪费,特别是在元素数量比较大的情况下,如下图:
数组由10扩容到15,添加一个元素后便不再添加,则后面的4个空间就浪费掉了。
ArrayList的类继承结构如下:
2、构造器
ArrayList有3个构造器,一个无参构造器和两个有参构造器。
2.1、ArrayList()
首先初始化一个空的Object数组,在首次添加元素时,使用Arrays.copyOf方法把原始的空数组拷贝成一个大小为10的数组。关键代码如下:
private void grow(int minCapacity/*这个就是初始化的容量10*/) {
//初始化的数组大小为0
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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);
}
2.2、ArrayList(int initialCapacity)
这个是指定初始容量的构造器,只要初始容量大于0,则初始化指定容量大小的数组,关键代码如下:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
2.3、ArrayList(Collection<? extends E> c)
这个是在初始化列表的同时把一个集合添加到ArrayList中,首先把c转为Object[],如果c的class不是Object[].class,则使用Arrays.copyOf方法把c拷贝为Object[]并重新赋值给ArrayList,关键代码如下: public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//若入参集合中元素为空,则初始化一个空的数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
3、关键方法
在讲述关键方法前,说一个比较重要的属性modCount,这个是从父类AbstractList继承过来的属性,是用来记录列表结构变更的次数,结构变更即那些更改列表size的方法,这个属性主要是在迭代过程中判断当前列表的结构有没有被变更过,若变更过则抛出ConcurrentModificationException异常。
3.1、add(E e)
添加指定元素到列表的末尾,modCount加1,首先确保列表当前的容量(即Object数组的长度)是大于或者等于size+1的,
private void ensureExplicitCapacity(int minCapacity/*传入的是size+1*/) {
modCount++;
// 若数组容量小于size+1,则需要增长容量
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
否则,首先计算新容量为原始容量的1.5倍,若新的容量还是小于传入的size+1,则使用size+1作为新容量,再判断新容量是否大于Integer.MAX_VALUE - 8(即允许的列表最大容量),若大于并且为负数,说明已经超出了整数范围,抛出OutOfMemoryError错误,最后把原始的Object数组使用Arrays.copyOf拷贝为一个长度为新容量的数组。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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);
}
在容量确保完成后,在elementData的size位置赋值指定的元素,并把size加1。
3.2、add(int index, E element)
这个重载的add方法与3.1的add方法很像,这个是可以在指定索引插入元素的方法,首先看一幅图:
上述的这种使用System.arraycopy移位方法在后面的很多方法中都使用的到。首先校验索引index范围必须是[0,index],modCount加1,然后确保容量满足size+1,不满足则扩容为原始容量的1.5倍,然后就按照上图的步骤来插入元素,最后size加1。关键代码如下:
public void add(int index, E element) {
rangeCheckForAdd(index);//校验index范围
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);//从index元素开始一直到末尾,整体后移一位
elementData[index] = element;
size++;
}
3.3、addAll(Collection<? extends E> c)
把指定集合中的所有元素追加到ArrayList的末尾,把c转为Object数组a,modCount加1,首先确保容量满足size+a.length,不满足则扩容,然后使用System.arraycopy把数组a拷贝到elementData的末尾,最后size累加上a.length。关键代码如下: public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
3.4、addAll(int index, Collection<? extends E> c)
在指定的索引位置插入指定集合中的所有元素,看下图:
首先检查index范围必须在[0,size]之间,modCount加1,确保容量满足size+c的大小,然后就是按照上图来进行移动,最后size加上c的大小,关键代码如下:
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;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
3.5、clone()
克隆出一个ArrayList,注意是浅克隆,ArrayList中的元素与原始列表中的元素是共享的。
3.6、indexOf(Object o)
若o为null,则循环elementData,找出第一个null并返回所在索引,不为空,则循环elementData,找出第一个equals入参o的索引并返回,没找到则返回-1。3.7、contains(Object o)
就是使用indexOf来判断元素是否存在,indexOf返回不为-1即元素存在,否则元素不存在。3.8、ensureCapacity(int minCapacity)
增加ArrayList的容量,只要minCapacity大于当前的ArrayList容量,就会使ArrayList扩容,并且会使modCount加1。推荐用法:在需要存入大量的元素前,根据元素个数和当前ArrayList的size来调用此方法,以确保ArrayList容量能满足待存入的大量元素,这样就可以减少在添加元素时的扩容,从而提高效率。3.9、forEach(Consumer<? super E> action)
这个是在jdk1.8添加的新方法,主要是用来循环ArrayList,使用每一个元素作为入参来调用action,配合jdk1.8的特性,可以很方便的使用方法引用和Lambda表达式来遍历执行ArrayList,例如:
@Test
public void test03() {
List<Integer> a = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
//使用方法引用
a.forEach(System.out::println);
System.out.println("-----------------------");
//使用lambda表达式
a.forEach(x -> System.out.println(x));
}
3.10、get(int index)
index必须小于size,返回elementData数组中index索引的元素。
3.11、iterator()
返回ArrayList自己实现的迭代器,这个迭代器相比AbstractList中的实现是优化过的。此迭代器实现了remove方法,可以在迭代过程中移除元素。在迭代期间会校验modCount是否被修改了,若被修改了则会抛出ConcurrentModificationException异常。3.12、listIterator()
返回ArrayList自己实现的列表迭代器,这个相比普通迭代器可以实现在迭代过程中,指针的向前移动,并且能够替换和添加元素。3.13、remove(int index)
关键步骤是使用System.arraycopy把ArrayList中index下一位至最后的元素,向左移一位,关键代码如下: 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; // clear to let GC do its work
return oldValue;
}
3.14、removeAll(Collection<?> c)
这个实现还是比较巧妙的,使用两个指针,r:表示当前迭代的元素索引,w:当前被保留的元素存储的索引,整个过程不创建新的数组,并且使用很少次数的System.arraycopy来移动数组元素。大概步骤:迭代ArrayList,若元素在c中存在,则不保留,否则存入到w所在位置上,并且w加1,其实就是把所有保留下来的元素集中到elementData前面,然后把w以后的所有元素设置为null,帮助垃圾回收,关键代码如下: private boolean batchRemove(Collection<?> c, boolean complement/*removeAll方法传入的是false*/) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
//把保留的元素集中到elementData前部
elementData[w++] = elementData[r];
} finally {
//r!=size是因为有可能前面调用c.contains时发生异常
if (r != size) {
//发生异常后需要把当前迭代位置r以后的元素全部保留下来,否则会导致ArrayList中的元素丢失
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
3.15、removeIf(Predicate<? super E> filter)
这个是jdk1.8添加的新方法,移除ArrayList中所有符合filter条件的元素,这个实现也比较的巧妙,首先循环ArrayList,然后使用BitSet来记录下符合filter条件的元素位置,然后再根据BitSet把需要保留的元素集中到elementData的前部,最后再把elementData后面不需要的元素设置为null,帮助垃圾回收,管家代码如下: public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}