ArrayList如何实现增删元素及其缺陷
ArrayList提供如下构造方法:
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
从构造方法我们可以知道, ArrayList使用的是数组来存储元素, 后续对ArrayList的增删操作都转化为对数组元素的操作.
但是数组的长度是在实例化的时候就确定的, 并且不停删除元素操作.那么, 对于ArrayList而言:
- 如何在运行时动态调整数组的长度?
- 如何实现删除元素操作?
增加元素方法如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
//modCount这个变量记录了当前实例元素结构的修改次数
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
新数组长度为原数组的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//数组拷贝得到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
大概操作逻辑如下:
- 首先确定当前数组长度是否还有空余位置, 只有满足:
(size + 1) - elementDate.length > 0
的时候才会进行数组拷贝, 然后使用拷贝的新数组替代原数组, 同时,
新数组长度为远来的数组的1.5倍, 这样也就实现在动态调整数组长度;
- 为新数组对于的index的元素赋值.
删除指定位置(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;
}
大概操作逻辑如下:
- index范围检查;
- 获取数组elementData 对应index位置上的元素;
- 然后计算哪些元素需要移动位置.例如:
对于一个长度为N的数组, 删除 index为M(0 <= M <= N-1)位置上的元素,
则需要将index为M+1至N-1上的所以元素位置向前移动一位; - 将index为size-1的元素的引用赋值为null.
从这来看, 我们大概可以看到删除元素的过程中底层始终是同一个数组, 只是通过移动元素在数组的位置来实现的,
同时将一个引用赋值为null. 使得引用指向的堆中对象所占据的空间可以被下一次GC回收掉.
这样实现还存在一个弊端:由于删除并未改变数组的引用, 所以数组对象占据的堆空间大小并未改变.
这个导致当size远小于elementData.length的时候, 数组对象依然占据很大一部分内存.
其实这个问题,可以设定一个阀值, 可以在删除元素的时候, size 与 elementData.length的差值达到阀值的时候,
在进行一次数组拷贝, 新数组长度就是size.