在末尾添加一个元素add( E e )
public boolean add(E e) {
ensureCapacityInternal(size + 1);//确保内部容量够用,必要的话进行扩容。否则elementData[size]将会数组越界
elementData[size++] = e;//相当于elementData[size] = e; size++;先赋值,再++。
return true;
}
从源码上看,添加元素之前调用ensureCapacityInternal(size+1)扩充容量。现在我们来看看具体的扩容操作
如何扩容 ensureCapacityInternal( int minCapacity )
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //从这里可以看出,只要添加了元素,ArrayList的容量最少为10
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)//当需要的容量大于当前容量时,进行扩容
grow(minCapacity);
}
private void grow(int minCapacity) { //扩容核心代码
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//容量扩充到原来的1.5倍,整除
if (newCapacity - minCapacity < 0) //如果扩容后的新容量还是没有传入的所需的最小容量大
//(主要在addAll(Collection e)方法中),以较大的也就是传入的容量为准
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//是否超过最大限制
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity); //扩充容量,将elementData数组的容量增加到newCapacity
// 并将原有数据复制过去
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从上述源码上看,add方法先调用ensureCapacityInternal扩充自身容量,如果扩充后的容量比常量DEFAULT_CAPACITY(10)小,则扩充到DEFAULT_CAPACITY,也就是说填充了元素的list最小容量为10。
如果扩充后的容量比数组elementData的长度大,那么调用grow()方法将数组大小增加为原来的1.5倍,如果1.5倍的数组大小还不够,那么将数组内存扩大到指定的容量大小。(MAX_ARRAY_SIZE=Integer.MAX_VALUE-8)
注意:一般情况下扩充到1.5倍是够的,因为add方法传入的最小扩容量为size+1, 1.5倍的数组大小肯定是够的,这里主要考虑到是addAll(Collection c),它传入的最小扩容量为size+c.length,如果c.length比较大,那么1.5倍的size可能不够,那么扩充的容量就以传入的为准。总得来说,1.5倍size和传入的最小扩容量取大的那个。
这里还考虑到了另一种极端情况:溢出。
int newCapacity = oldCapacity + (oldCapacity >> 1)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
如果oldCapacity非常大,接近了MAX_ARRAY_SIZE.那么1.5倍的oldCapacity也就是newCapacity可能会发生溢出,newCapacity可能会变成一个负数。那么newCapacity - MAX_ARRAY_SIZE > 0 成立(一个比最小负数还小的数会变成正数,这也是溢出的一种),那么执行hugeCapacity(minCapacity)进行溢出处理。如果将代码改成
if (newCapacity > MAX_ARRAY_SIZE )那么对溢出情况就会遗漏。
另外,容量的扩展会执行整个数组内容的复制( Arrays.copyOf(elementData, newCapacity);),如果能够大概预知list的容量在创建的时候指定初始容量,或者调用ensureCapacity(ensureCapacity),减少扩容次数,能够有效提高效率。
注意上面的方法是私有的,jdk还提供了一个共有的扩容方法ensureCapacity(ensureCapacity)
公有扩容方法:ensureCapacity(int minCapacity)
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0 : DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
ensureExplicitCapacity方法:
在如何扩容 ensureCapacityInternal( int minCapacity )
中讲过
在指定位置插入元素add( int index, E element )
public void add(int index, E element) {
//检查索引范围
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//检查索引范围,在0到size之间
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
首先判断索引index是否合法,然后调用ensureCapacityInternal调整容量(容量不够会进行扩容),之后调用System.arraycopy进行数组复制,将elementData从index位后面的数据复制到index+1位置上,也就是index之后的数据统一向后挪一位,然后将element复制到elementData[index]位置上。
这里说明一下
System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
arraycopy可以执行数组的复制,它是一个本地方法。src为源数组,dest为目的数组,srcPos为源数组要复制的起始位置,destPos为目的数组放置的目标位置,length为要复制的长度。src和dest为同一个数组时,可以复制自身数组。当然前提时保证目的数组的长度足够,ArrayList先调整了容量,不会出现数组溢出的情况( java.lang.ArrayIndexOutOfBoundsException)。
将集合c添加到list的尾部 addAll( Collection< ? extends E > c )
/**按照c的迭代器所返回的元素顺序,将元素添加到list的尾部
@param c collection containing elements to be added to this list
@return <tt>true</tt> if this list changed as a result of the call
@throws NullPointerException if the specified collection is null
**/
public boolean addAll(Collection<? extends E> c) {
//调用c.toArray()将从转换为对象数组
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
先将c转换为对象数组a,然后调整容量,调用system.arraycopy将a的内容复制到elementData尾部。有一个注意的点,它的返回值是看list是否被改变;如果c为null,将报空指针异常。
使用场景:
如果数据量很大,那么推荐使用addall。原因如下:
system.arraycopy在内存中进行复制,速度快。
system.arraycopy是本地方法,执行效率高。
如果数据量比较小,几十个左右。可以用for循环来添加。
addAll( int index, Collection< ? extends E > 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;
}
执行逻辑跟addAll类似,不过增加了一步
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
//将数组缓存中index后面的数据向后挪numNew位(numNew为c的长度),
//为下一步将c中的数据插入到elementData中让出位置,否则会被覆盖掉。
删除指定位置的元素并返回该元素remove( int index )
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their indices).所有后续元素左移,索引减1
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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; // 先执行size--,在执行elementData[size] = null。末尾元素置空,等待垃圾回收
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
首先检查索引范围,修改次数加1。保留要被移除的元素,将index后面的数据左移一位,size减1,将末尾元素置空,返回被移除的元素。
移除第一次出现的指定对象remove( Object o )
/**
* Removes the first occurrence of the specified element from this list,if it is present.
* If the list does not contain the element, it is unchanged. More formally, removes the element with the lowest index.
* @param o element to be removed from this list, if present
* @return true if this list contained the specified element
*/
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; // clear to let GC do its work
}
首先判断传入对象o是否为null,如果是null,for循环遍历缓存数组,如果找到某个元素为null,则删除该元素,size-1并返回true。如果o不为null,for循环遍历缓存数组,通过equals判断如果有某个元素等于指定对象o,则删除该对象size-1并返回true。如果找不到该对象,返回false。
这里通过对象的equals方法判断元素是否为某个对象。
删除所有在指定的集合c中包含的元素 removeAll( Collection< ? > c )
/**
* Removes from this list all of its elements that are contained in the specified collection.
* @param c collection containing elements to be removed from this list
* @return {@code true} if this list changed as a result of the call
* @throws ClassCastException if the class of an element of this list is incompatible with the specified collection
* @throws NullPointerException if this list contains a null element and the specified collection does not permit
* null elements or if the specified collection is null
*/
public boolean removeAll(Collection<?> c) {
if (c == null)
throw new NullPointerException();
return batchRemove(c, false);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
//r先右移
for (; r < size; r++)
// 如果c中不包含elementData[r]这个元素
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];// 则直接将r位置的元素赋值给w位置的元素,w自增。保留不在c中的元素
} finally {
if (r != size) {//c.contains(elementData[r])出现了异常,for循环没有执行到最后
System.arraycopy(elementData, r,
elementData, w,
size - r);// // 将r未右移完成的位置的元素赋值给w右边位置的元素
w += size - r; // 修改w值增加size-r
}
if (w != size) { // w==size表示所有元素都保留了下来,说明没有删除任何元素。w!=size则有部分元素被删除,
//将w后面的元素置空,modified=true。说明删除成功,列表被改变了。w前面的元素就是保留下来的元素。
// 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;
}
遍历elementData,如果c不包含elementData的元素e,那么elementData[w] = e;将c中不包含的数据从头到尾保留下来,也就变相删除了在c中包含的数据。finally处理c.contains(elementData[r]出现异常的情况,因为有些集合不允许null值,c.contains()可能会抛出异常。finally保证了抛出异常之前的操作。
慎用 removeAll( Collection< ? > c )
- 内部使用双层for循环,c.contains()方法经常也是一个循环遍历。效率低下。
例如现有主ArrayList a:2万条数据,子ArrayList b : 1万条数据。那么调用a.removeAll(b)将循环2万*1万次。效率及其地下。 - finally块中对异常情况进行了特殊处理,保证了抛出异常之前的操作。但我们一般的主观印象是一旦出现异常,一切应该回到原点(类似于事务)
调用a.removeAll(b),如果出现异常,我们主观上可能会认为a会保持不变。但实际上在抛出异常之前的操作会被保留,这可能会导致不可预知的错误。
举例说明:现有ArrayList< Integer > a={1,2,3,4,5,6,7,8,9,null,10,11,12,13},
TreeSet b = {1,2,3,12,13}。 TreeSet调用contains(null),会抛出异常。
那么执行
a.removeAll(b)。在循环到a[9],也就是null的时候,抛出异常。但在抛出异常之前,已经删除了1,2,3三个元素,这个操作会被保留下来。最终结果是抛出了异常,但a已经被改变了。
删除指定索引范围内的数据 removeRange( int fromIndex, int toIndex )
/**
* Removes from this list all of the elements whose index is between {@code fromIndex}, inclusive,
* and {@code toIndex}, exclusive.
* Shifts any succeeding elements to the left (reduces their index).所有后续元素左移,索引减1
* This call shortens the list by {@code (toIndex - fromIndex)} elements.
* (If {@code toIndex==fromIndex}, this operation has no effect.)
*
* @throws IndexOutOfBoundsException if {@code fromIndex} or
* {@code toIndex} is out of range
* ({@code fromIndex < 0 ||
* fromIndex >= size() ||
* toIndex > size() ||
* toIndex < fromIndex})
*/
protected void removeRange(int fromIndex, int toIndex){
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
注意这是一个protected的方法,并不是public。如果要达到相同的效果,可以调用subList(fromIndex,toIndex).clear(),效果是一样的。