ArrayList原理分析

632 阅读8分钟

本文以Java8为例

从ArrayList实现的接口说起

查看ArrayList源码会发现ArrayList继承了AbstractList类,实现了List, RandomAccess, Cloneable, java.io.Serializable接口。

AbstractList

AbstractList是抽象类,里面有众多List基本方法的基本实现。事实上JDK中有众多类似AbstractXXX的类,他的思想是就是把众多公共的实现提取出来,AbstractXXX面向的是工具类的开发者,我们可以基于AbstractList来实现自己的List。而ArrayList只是JDK已经帮我们实现好的,面向工具类的使用者。

List

List结构顶级接口,

RandomAccess

RandomAccess即随机访问。

先给出结论:集合中元素的访问分为随机访问和顺序访问。 随机访问类似于数组下标的访问,顺序访问类似于链表的访问,随机访问直接通过下标取值性能较好,顺序访问以迭代器遍历性能比较好。 查看RandomAccess接口,仅仅是一个空接口而已。

查看调用RandomAccess接口的类,发现除了实现RandomAccess,其他都是用于instaceof判断。

原来,RandomAccess的意义仅仅是一个标记而已,即标记接口,用于判断List是否为RandomAccess的实现。以Collections的binarySearch方法为例:

二分查找先判断list是否是RandomAccess的子类或大小是否小于阈值5000,如果为真则会执行indexedBinarySearch

会发现indexedBinarySearch是以数组下标的方式获取值。如果为假会执行iteratorBinarySearch

iteratorBinarySearch是以迭代器的方式获取值。即JDK以性能方面的考虑,如果list支持随机访问或size比较小,则以下标索引的方式获取内容,否则以迭代器的遍历获取内容。

所以RandomAccess仅仅是一个类的标记。

Cloneable

Cloneable是否可克隆,查看Cloneable接口 查看调用Cloneable接口的类,都是实现Cloneable接口。与RandomAccess类似,Cloneable接口也是标记接口,用于判断该类的对象是否可克隆。

这里会有一个疑问:什么是可克隆?

先给出结论:clone方法是基类Object的方法,只有实现了Cloneable接口的类才可以使用clone方法,否则会抛CloneNotSupportedException异常。下面是某版本JDK的clone方法的源码,会发现如果不是Cloneable的子类则会抛异常。

protected Object clone() throws CloneNotSupportedException {
    if (!(this instanceof Cloneable)) {
        throw new CloneNotSupportedException("Class " + getClass().getName() +
                                             " doesn't implement Cloneable");
    }

    return internalClone();
}

/*
 * Native helper method for cloning.
 */
private native Object internalClone();

然而查看JDK1.8的clone方法,仅仅是一个本地方法而已,他又是怎么做到抛异常的呢?

protected native Object clone() throws CloneNotSupportedException;

原来在JDK1.8中,如果我们查看JVM的C++源码,会发现“对象是否实现了cloneable接口抛异常”的动作实现在了JVM层中。

所以ArrayList通过实现了cloneable接口,支持clone方法。

另外,有说法认为Java的类通过实现Cloneable来标识出对象是否可以使用clone是一种很糟糕的设计,未实现Cloneable确对外暴露clone方法的规则很奇怪。

java.io.Serializable

同样也是标记接口,表示该类的对象是否可以序列化。

成员属性

// 若初始化时未指定容量大小,默认为10
private static final int DEFAULT_CAPACITY = 10;

// 一个空数组而已
private static final Object[] EMPTY_ELEMENTDATA = {};

// 也是一个空数组,只不过与EMPTY_ELEMENTDATA做区分。
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA表示初始化是无参构造
// 而EMPTY_ELEMENTDATA是有参构造,只不过参数是0
// 之所以用来区分是因为无参构造是默认容量为10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存储对象的容器
transient Object[] elementData;

// 存储对象的多少
private int size;

主要方法

构造方法

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;
}

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 {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

三个构造方法参数分别是容量、空、集合。通过观察代码会发现容量为0时, this.elementData有两种情况

构造函数传入initialCapacity时this.elementData = EMPTY_ELEMENTDATA;

无参构造函数时this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

关于这两个属性,源码给的解释是

 /**
 * Shared empty array instance used for empty instances.
 * 用于空实例的共享空数组实例
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 * 共享空数组实例,用于默认大小的空实例。
 * 我们将其与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时应该膨胀多少
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA其实就是实例化ArrayList方式的区分。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA在后面add方法中会详细解释,其实这里可以理解为DEFAULTCAPACITY_EMPTY_ELEMENTDATA表示实例化时是无参构造,未指定容量,在调用add方法时这种情况会默认此刻容量为10。

而EMPTY_ELEMENTDATA表示在我们实例化对象时指定了容量就是0。

add方法

public boolean add(E e) {
    // 内部确保容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}


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++;
}

add方法的核心ensureCapacityInternal,内部确保容量。

内部确保容量即add元素时,程序里通过当前的size自动判断当前是否需要扩容。与其相对应的是显示确保容量。ensureCapacityInternal源码分析如下

// minCapacity表示容器要确保支持的容量
// elementData的长度至少要大于等于minCapacity,是需求的最低大小
// 比如add时 minCapacity就是size+1
// ensureCapacity调用ensureCapacity显示扩容时,minCapacity就是传入的参数
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 该方法仅仅是多做了一步判断,如果是以new ArrayList()无参构造函数实例化的对象,且是第一次add元素,则默认的最小容量为10。
// 如果没有默认容量,因为容器最初容量小,add元素时1.5倍的扩容方式会造成频繁的扩容。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 显示确保容量,在ensureCapacityInternal中调用只是方法的公用
// “显示”的含义主要体现在ensureCapacity方法的调用,ensureCapacity是人为的显示扩大容量
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 实际扩容,默认按1.5被扩容,如果1.5倍还是小于minCapacity,则以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);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

如果未指定容量,add方法以10为初始值,每次扩容以1.5倍扩容。所以如果有大量数据add,也会触发多次扩容。而每次扩容实际上是将数组内容复制到新的数组里。

trimToSize

精简化容器。ArrayList在实际使用中,elementData的长度大于等于size,而trimToSize会将elementData按照size重新复制一份给elementData,最小化ArrayList实例的存储。如果实例内容不变,可以调用该方法节约内存。

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
                ? EMPTY_ELEMENTDATA
                : Arrays.copyOf(elementData, size);
    }
}

ensureCapacity

相当于扩容,也是我们前面提到的显示确保容量。

给定一个minCapacity,保证ArrayList可以容纳至少minCapacity数量的对象。

public void ensureCapacity(int minCapacity) {
    // 以此来判断我们实例化时是否为 new ArrayList()
    // 如果是,他的默认长度在add时就是10,不需要扩容
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0
            : DEFAULT_CAPACITY;
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

clone

前面已经做过分析,通过实现Cloneable接口,使得clone方法可用。重写clone方法,复制出一份数组,但其本身还是浅拷贝,数组里引用的对象没有被复制。

modCount表示改动次数,新克隆的对象置为0。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

remove

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;
}

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
}

remove其实就是将index后面的元素向前移动一位。所以若ArrayList很大,不适合使用remove方法。

与Vector对比

Vector就是线程安全版的ArrayList,它的实现与ArrayList基本类似,这里说下比较明显的3点区别

Vector线程安全

Vector针对容器的曾删改查操作都加上了synchronized关键字来保证线程安全,譬如:

public synchronized void ensureCapacity(int minCapacity) {
    if (minCapacity > 0) {
        modCount++;
        ensureCapacityHelper(minCapacity);
    }
}

public synchronized void insertElementAt(E obj, int index) {
    modCount++;
    if (index > elementCount) {
        throw new ArrayIndexOutOfBoundsException(index
                                                 + " > " + elementCount);
    }
    ensureCapacityHelper(elementCount + 1);
    System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
    elementData[index] = obj;
    elementCount++;
}
    
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

构造方法略有不同

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
} 

相比ArrayList,Vector多了一个构造方法,多传入一个参数capacityIncrement,可以用来指定每次扩容的增量。

另外new Vector()默认容量也是10,只不过在实例化时数组的长度就已经是10了。而ArrayList中是add第一个元素之后数组的长度才会变成10。

public Vector() {
    this(10);// 默认容量为10
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

扩容规则略有不同

Vector的扩容方法

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 如果有capacityIncrement,按照capacityIncrement扩容,否则按照当前容量的一倍扩容
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
            capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

Vector的扩容默认是按一倍扩容,如果指定capacityIncrement,则按照capacityIncrement扩容。

Vector无论是读还是写都需要加锁,对于读多写少的场景,可以使用CopyOnWriteArrayList。

与LinkedList对比

LinkedList就是个双向链表结构,每个节点维护其前置节点和后驱节点。

ArrayList是线性结构支持RandomAccess,LinkedList是链式结构。

总结

实现RadomAccess随机访问,使得ArrayList在一些方法中是以数组下标取值。

实现CloneAble支持克隆,ArrayList浅拷贝。

默认容量为10,以1.5倍递增的方式扩容。

数据量大时并不适合remove。

Vector是线程安全的List,但无论是读还是写都需要加锁,对于读多写少的场景,可以使用CopyOnWriteArrayList。