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而言:

  1. 如何在运行时动态调整数组的长度?
  2. 如何实现删除元素操作?

增加元素方法如下:

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.

posted @ 2018-11-17 14:52  index_1  阅读(1492)  评论(0编辑  收藏  举报