一、源码总结
在源码类的注释中提到ArrayList
用可伸缩数组实现的这个List接口,继承自AbstractList
,AbstractList
实现了List
接口,提供了List
接口的默认实现。ArrayList
自身也实现了List
接口,实现了对集合的CURD的操作,实现了RandomAccess
接口,支持快速随机访问,实际上就是通过下标进行快速访问,RandomAccess
是一个标记接口,接口内没有定义任何内容。
ArrayList
的源码中多次调用了两个数组拷贝的方法,分别是Arrays.copyOf
和System.arraycopy
,查看这两个方法的源码。
public static T[] copyOf(T[] original, int newLength) {return (T[]) copyOf(original, newLength, original.getClass());
}
public static 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);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));return copy;
}
该方法创建了一个新的数组,然后调用System.arraycopy
将元素拷贝到新数组中。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,int length);
arraycopy
是一个本地方法,通过调用系统的C/C++
方法实现。该方法可以保证在同一个数组内的元素的复制和移动。
通过源码的分析,可以更加深刻的理解为什么说ArrayList
的查找效率高而增加、删除元素的效率低。
二、扩容
添加元素时使用ensureCapacityInternal()
方法来保证容量足够,如果不够时,需要使用grow()
方法进行扩容,新容量的大小为 int newCapacity = oldCapacity + (oldCapacity >> 1)
,也就是旧容量的 1.5 倍。
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量以确保其至少可以容纳最小容量参数指定的元素数量。
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
// oldCapacity 的值为 elementData 中数组长度值
int oldCapacity = elementData.length;
// 使用位运算提高运算速度,newCapacity 是 oldCapacity 的 1.5 倍。
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果 oldCapacity 的 1.5 倍还比 minCapacity 小,那么 newCapacity = minCapacity
if (newCapacity - minCapacity 0)
newCapacity = minCapacity;
// 如果 oldCapacity 的 1.5 倍比 MAX_ARRAY_SIZE 大,那么调用 hugeCapacity 做点事情
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// minCapacity 的值通常接近于 size 的值,所以这就是个胜利(节省空间)
// 最终 elementData 指向复制了 newCapacity 的新数组对象
elementData = Arrays.copyOf(elementData, newCapacity);
}
在添加很多元素前如果知道需要添加的元素数量,尽量先调用ensureCapacity()
先扩容数组到指定大小,再进行数据的添加,避免多次调用grow()
方法进行扩容。
三、部分方法示例
一、trimToSize()
如果没有新元素添加到集合中,则可以使用此方法最大程度地减少集合的内存开销。
/**
* Trims the capacity of this ArrayList instance to be the
* list's current size. An application can use this operation to minimize
* the storage of an ArrayList instance.
* 调整 ArrayList 实例的 capacity 为列表当前的 size。
* 程序可以使用这个方法最小化 ArrayList 实例的存储(节省不需要的空间)。
*/
public void trimToSize() {
//修改次数加1
modCount++;
// 在 size 小于数组的长度(数组中存在 null 引用)前提下
// 如果 size == 0 ,说明数组中都是 null 引用,就让 elementData 指向 EMPTY_ELEMENTDATA,
// 否则,就拷贝 size 长度的 elementData 数组元素,再让 elementData 指向这个数组对象
if (size elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
在ArrayList
源码中用size
表示实际存储元素的大小,elementData.length
表示数组的大小,trimToSize()
方法就是最小化 ArrayList 实例的存储(节省不需要的空间),因为ArrayList 只提供了获取size
大小的方法,并没有提供获取elementData.length
的方法所以并不能很直观的看到该方法的作用,但是可以用debug看到实际变化。
@Test
public void test1(){
ArrayList list = new ArrayList<>(10);
list.add("1");
list.add("2");
list.add("3");
list.trimToSize();
list.forEach(System.out::println);
}
上边方法在和list.trimToSize();
上打断点,运行方法跟进debug看到elementData.length
的大小为10,size
大小为3
方法运行完成后elementData.length
的大小为3,size
大小为3。
trimToSize();
执行完成后还可以继续做add()
操作,但是并不建议这么做,因为需要重新扩容才能添加元素,所以要在确保不再添加元素时再进行调用该方法。
二、clear()
/**
* Removes all of the elements from this list. The list will
* be empty after this call returns.
* 从此列表中删除所有元素。 该调用返回后,该列表将为空。
*/
public void clear() {
//修改次数加1
modCount++;
//方便GC回收将所有元素指向 null
// clear to let GC do its work
for (int i = 0; i elementData[i] = null;
//大小修改为0
size = 0;
}
clear()
方法会清除所有元素,这里有个疑问为什么不直接elementData= null;
而是将elementData中的每个元素指向null
。
去网上找了下感觉回答的挺好的原文.
咋一看起来,好像下面的方法更简单:
modCount++;
elementData = EMPTY_ELEMENTDATA;
size = 0;但是有一个原因让这种做法不可行:
elementData
不是私有的!在设计上,为了效率,
elementData
是包可见的,会被其他类(主要是ArrayList
的内部迭代器类)直接引用。如果采用上面的做法,就可能会导致迭代器与数据不一致问题。