ArrayList学习

来源:互联网 发布:史记精彩片段 知乎 编辑:程序博客网 时间:2024/05/17 06:45

转自:http://my.oschina.net/ostatsu/blog/535669

定义

ArrayList 类是 List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

结构

public class ArrayList<E> extends AbstractList<E>        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

继承关系如下:

java.lang.Object    java.util.AbstractCollection<E>        java.util.AbstractList<E>            java.util.ArrayList<E>

并实现了 List , RandomAccess , Cloneable , Serializable 这 4 个接口。

AbstractList

AbstractList 抽象类使用了缺省适配(Default Adapter)模式,提供了对 List 接口的缺省实现,使其子类(ArrayList)可以直接从其进行扩展,而不必从 List 接口进行扩展。
篇幅所限,不单独分析 AbstractList 类的源码,本文之后与 ArrayList 类的源码一同分析。

RandomAccess

RandomAccess 是 List 的实现所使用的标记接口,用来表明其支持快速(通常是固定时间)随机访问。
个人简单的理解,就是当集合类使用 get(int index) 方法获取指定位置的元素时,若其内部实现不需要遍历集合,则其访问集合元素的单位时间是一定的,不会因索引的变化而变化,时间复杂度为 O(1),而通常这种方式效率更高,便需实现 RandomAccess 接口,标记其支持快速随机访问。

成员变量

    /**     * 数组的默认初始化容量。     */    private static final int DEFAULT_CAPACITY = 10;    /**     * 共享空数组实例。     */    private static final Object[] EMPTY_ELEMENTDATA = {};    /**     * 内部数组。     */    private transient Object[] elementData;    /**     * 数组包含的元素个数。     */    private int size;

可以看到,ArrayList 集合实际上就是用一个 Object 数组 elementData 来存储元素的,用 size 记录数组包含的元素个数。

适配器模式

个人认为,ArrayList 类算是一个适配器类,使用了对象的适配器(Adapter)模式进行设计。其把数组对象适配成了客户端所期待的 List 接口,适配器模式所涉及角色如下:
目标(Target)角色:List。
源(Adaptee)角色:Object 数组 elementData。
适配器(Adapter)角色:ArrayList。
虽然数组不是一个类或接口,与适配器模式的定义不符,但如果把“接口”看做一个抽象的概念:一个实体所提供的方法,那么就成立了。至少使用适配器模式来理解,个人认为比较直观。

构造方法

无参构造方法

    /**     * 构造一个空列表。     */    public ArrayList() {        super();        this.elementData = EMPTY_ELEMENTDATA;    }

在一般情况下使用无参构造方法初始化一个 ArrayList 实例时,将空数组实例 EMPTY_ELEMENTDATA 赋给 elementData 数组。在 JDK 1.6 时,其实现方法为 this(10); ,即调用下面的指定初始容量构造方法并赋值 10,在 JDK 1.7 时改变了这种设计,默认初始化并不指定数组容量。但其源码的注释中并未更改过来,依然是:Constructs an empty list with an initial capacity of ten.

指定初始容量构造方法

    /**     * 构造一个具有指定初始容量的空列表。     */    public ArrayList(int initialCapacity) {        super();        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal Capacity: "                    + initialCapacity);        this.elementData = new Object[initialCapacity];    }

可以使用带有指定初始容量的构造方法构造一个 ArrayList 实例,传入初始容量,正常情况下会使用传入的容量对 elementData 进行初始化。在确定一个 ArrayList 集合的元素数量时可使用此方法,以便防止时间和空间资源的浪费。

使用指定集合构造

    /**     * 构造一个包含指定集合中元素的列表。     */    public ArrayList(Collection<? extends E> c) {        elementData = c.toArray();        size = elementData.length;        if (elementData.getClass() != Object[].class)            // c.toArray might (incorrectly) not return Object[] (see 6260652)            elementData = Arrays.copyOf(elementData, size, Object[].class);    }

使用一个集合构造 ArrayList 实例,直接使用集合类的 toArray() 方法,将集合类的元素转成数组直接赋给 elementData,若其返回的不是 Object[] 数组,则使用 Arrays.copyOf 方法将其转换。在 ArrayList 中的实现此构造方法效率较高,在 LinkedList 中需要调用 addAll 方法,效率比较低。
另外,toArray 方法是安全的,集合类并不维护对返回数组的任何引用,所以使用此方法不用担心修改 ArrayList 内元素会影响原集合类。
Collection 的 toArray 方法定义的返回值是 Object[],那么为何需要对其类型进行判断?而注释中也说了其可能错误的没有返回 Object[],查询后才知道这是一个 BUG(编号:6260652),详情点击此处。

添加元素

    /**     * 在此列表的尾部添加指定元素。     */    public boolean add(E e) {        ensureCapacityInternal(size + 1);        elementData[size++] = e;        return true;    }    /**     * 在此列表的指定位置插入指定元素。 将当前处于该位置的元素和所有后续元素向右移动。     */    public void add(int index, E element) {        rangeCheckForAdd(index);        ensureCapacityInternal(size + 1);        System.arraycopy(elementData, index, elementData, index + 1, size                - index);        elementData[index] = element;        size++;    }    /**     * 将指定集合中的所有元素都添加到此列表的结尾。     */    public boolean addAll(Collection<? extends E> c) {        Object[] a = c.toArray();        int numNew = a.length;        ensureCapacityInternal(size + numNew);        System.arraycopy(a, 0, elementData, size, numNew);        size += numNew;        return numNew != 0;    }    /**     * 将指定集合中的所有元素都插入到列表中的指定位置。     */    public boolean addAll(int index, Collection<? extends E> c) {        rangeCheckForAdd(index);        Object[] a = c.toArray();        int numNew = a.length;        ensureCapacityInternal(size + numNew);        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;    }

源码都比较简单,不做过多分析。有几点说明:
增加元素需要变更 size;
指定位置时需要使用 rangeCheckForAdd 进行范围检查;
需要调用 ensureCapacityInternal 方法来确保容量;
大量使用 System.arraycopy 方法对数组进行拷贝,虽然此方法是个 native 方法,据说是直接复制内存中的数据库块进行拷贝,比用 Java 自己实现数组拷贝的效率高,但从代码的角度讲时间复杂度并不低。

扩容

添加操作的实现需要调用 ensureCapacityInternal 方法来确保容量,相关方法如下:

    private void ensureCapacityInternal(int minCapacity) {        if (elementData == EMPTY_ELEMENTDATA) {            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity);    }    private void ensureExplicitCapacity(int minCapacity) {        modCount++;        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;    private void grow(int minCapacity) {        int oldCapacity = elementData.length;        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);    }    private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0)            throw new OutOfMemoryError();        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE                : MAX_ARRAY_SIZE;    }

我在面试时就被问到过 ArrayList 是如何扩容的,当时没有答出来,现在来分析一下。
当调用任何需要确保容量后才能执行的操作时,都需要调用 ensureCapacityInternal(int minCapacity) 方法并传入需要的最小容量。此方法对需要的最小容量进行简单的处理(判断当 elementData 为空数组时将最小容量赋值为 DEFAULT_CAPACITY 与 minCapacity 之大者,这样做是为了防止当传入的最小容量较小时下次添加需要继续扩容影响效率),继续调用 ensureExplicitCapacity(int minCapacity) 方法。
ensureExplicitCapacity 方法首先对 modeCount 自增(modCount 是在 AbstractList 中定义的成员变量,记录列表发生结构性变化的次数,实际上是为了防止在多线程环境下对列表进行迭代遍历时,列表发生改变。之后会在分析迭代器的时候继续说明),当所需最小容量超出数组的当前大小时,此时确定需要扩容,则调用 grow(int minCapacity) 方法进行扩容。
grow 方法会先把新容量设定为旧容量的 1.5 倍左右,以防止扩容较少,下次还需继续扩容影响效率。当新容量还不能满足所需最小容量时,则将新容量赋值为所需最小容量。继续判断溢出,若新容量大于了 MAX_ARRAY_SIZE ,则调用 hugeCapacity(minCapacity) 方法,将容量赋值为 Integer.MAX_VALUE。
这里 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,意思是因为某些虚拟机需要在数组前加些头信息,所以把最大值缩小一些。但当需要容量比较大时就可尝试去掉这些限制,杯水车薪啊。实际应该几乎不会执行到 hugeCapacity 方法。
最后使用 Arrays.copyOf 方法传入新容量进行扩容。
以上方法均为私有方法,ArrayList 还提供了一些与容量相关的公开方法:

    /**     * 将容量调整为列表的当前大小。     */    public void trimToSize() {        modCount++;        if (size < elementData.length) {            elementData = Arrays.copyOf(elementData, size);        }    }    /**     * 确保容量。     */    public void ensureCapacity(int minCapacity) {        int minExpand = (elementData != EMPTY_ELEMENTDATA)        // any size if real element table        ? 0                // larger than default for empty table. It's already supposed to                // be                // at default size.                : DEFAULT_CAPACITY;        if (minCapacity > minExpand) {            ensureExplicitCapacity(minCapacity);        }    }

源码比较简单,不做分析。在 JDK 1.6 中,ArrayList 内部只使用了 ensureCapacity 方法来扩容,在 JDK 1.7 中才分成了多个方法,并把内部使用的方法命名为 ensureCapacityInternal。

获取元素

    public E get(int index) {        rangeCheck(index);        return elementData(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;    }    /**     * 移除此列表中首次出现的指定元素。     */    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 remove method that skips bounds checking and does not return the     * value removed.     */    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    }    /**     * 从此列表中移除那些也包含在指定集合中的所有元素。     */    public boolean removeAll(Collection<?> c) {        return batchRemove(c, false);    }    /**     * 从此列表中移除那些未包含在指定集合中的所有元素。     */    public boolean retainAll(Collection<?> c) {        return batchRemove(c, true);    }    private boolean batchRemove(Collection<?> c, boolean complement) {        final Object[] elementData = this.elementData;        int r = 0, w = 0;        boolean modified = false;        try {            for (; r < size; r++)                if (c.contains(elementData[r]) == complement)                    elementData[w++] = elementData[r];        } finally {            // Preserve behavioral compatibility with AbstractCollection,            // even if c.contains() throws.            if (r != size) {                System.arraycopy(elementData, r, elementData, w, size - r);                w += size - r;            }            if (w != size) {                // 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;    }

remove 的两个方法比较简单,就不说了。
removeAll 和 retainAll 方法在 JDK 1.6 时并未在 ArrayList 内实现,直接使用了父类 AbstractCollection 提供的默认实现。在 JDK 1.7 中优化了此方法。它们调用 batchRemove(Collection<?> c, boolean complement) 方法,使用第二个布尔值参数来判断是哪个方法的调用。try 语句块中的 if 条件判断的元素是保留的元素,把保留的元素依次放到数组中。finally 语句块中,若上面 c.contains 抛出异常,则第一个 if 判断生效,尽可能恢复数据。第二个 if 判断生效时清除后面的空间。





0 0
原创粉丝点击