源码分析-java-ArrayList-基本方法及实现

来源:互联网 发布:mui框架 js自动加载 编辑:程序博客网 时间:2024/05/16 05:43

ArrayList

API文档

大小的可变数组的List接口的实现。实现了所有的List可选操作,允许所有(包括null)在内的元素。为了实现这List接口,这个类提供了操作数组(这个数组用于内部储存)大小的方法。(这个类类似于Vector,只是这个类不是同步的)

size、isEmpty、get、set、Iterator和ListIterator以固定的时间运行。add操作的摊还时间为添加n个操作需要O(n)时间。所有其他操作均运行在线性时间(大约的情形)。常数因子略低于LinkedList的实现

每个ArrayList实例都有一个capacity。这个能力表示数组能储存的元素的大小。它至少大于size。随着元素加入到ArrayList。它的capacity自动增长。增长策略的细节基于这样的细节,一个元素有常数的均摊时间代价。

应用程序应该在大量添加元素之前,使用ensureCapacity操作增加ArrayList的capacity。这可能会降低请求内存的次数。

需要注意的是这个实现不是同步的。如果多线程同时访问ArrayList实例,而且至少有一个线程结构性的更改了list,它必须要由额外的同步策略。通常来说通过同步一些代码块实现。如果没有这样的对象存在,则list应该被Collections.synchronizedList方法包装起来。为了防止意外的异步访问这个list,包装最好在创建的时候包装。

List List = Collections.synchronized(new ArrayList)

通过这个类的Iterator方法和listIterator方法返回的迭代器是fail-fast的。如果在迭代器创建后,不同此迭代器而list实现了结构性调整,这个迭代器会抛出一个ConcurrentModificationException异常,因此在面对同时修改的可能时,迭代器迅速失效,而不是增加风险的随机性、及未来不可预估的行为和时间。

需要说明的是,fail-fast行为的迭代器通常来说不能强制性的保证不发生异步的同时修改。fail-fast迭代器竟可能保证快速抛出异常。因此依赖fail-fast迭代器的抛出异常行为的正确性写出代码是不可靠的。迭代器的fail-fast行为应该只用于debug。

构造器及相关域

private static final long serialVersionUID = 8683452581122892189L;private static final int DEFAULT_CAPACITY = 10;private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData; // non-private to simplify nested class accessprivate int size;

首先是四个静态域:

  • serialVersionUID :没什么好说的。
    DEFAULT_CAPACITY 表示默认的初始化数组大小。
  • EMPTY_ELEMENTDATA: 是一个静态的空数组,意思是当一个ArrayList在创建时先将其指向这里(除非是传入是集合)。直到ArrayList添加了第一个元素之前一直都指向这里。而不是真的创建一个数组。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA: 作用同EMPTY_ELEMENTDATA 一样。区别是这里表明创建这个类的时候没有指定capacity而是使用默认的DEFAULT_CAPACITY 。
  • elementData:是真实的存放元素的地方。有意思的是这里居然是使用默认的访问权限,理由是方便内部类的访问。但是按照道理来说即使是private内部类也可以访问。不知道为何这里要使用默认的访问权限。此外这个类是transient的,这里也很有意思。

构造器

public ArrayList()public ArrayList(int initialCapacity)public ArrayList(Collection<? extends E> c)

ArrayList():直接将elementData指向了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ArrayList(int initialCapacity):检查了initialCapacity的合法性,然后将elementData指向了
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;        }    }

首先将原集合转成了数组。然后判断是否是空,如果是空则将elementData指向EMPTY_ELEMENTDATA空数组。
在不是空的情况下这里做了一些额外的判断。
首先如注释所说这里有一些java的bug。在有些情况下toArray返回的并不是Object[]类的数组。如果toArray返回的不是Object数组,而是一个Object子类的数组,那后期如果向其添加非该子类或者其子类的元素会导致错误,更为关键的是这样的错误应用程序相对来说难以排查。所以这里判断了elementData的类型是不是Object[]类型,如果不是则调用Arrays的copyOf静态方法将elementData复制到一个同样大小的Object[]中。

这里补充下关于java的bug的问题:
java对于bug建立了一个bug库,这里的注释 see 6260652就是bug的编号。网上搜一下就很容易搜到。
比如这里的bug其实说的是Arrays.asList(String[])方法返回的并不是Object类而是一个String相关的类型。这样当你在存入Object类的时候(或者只要存入类型不是返回类型或者其的子类)的时候会出现错误。类似的还有Arrays.ArrayList.toArray()
bug出现的概率是很低的。目前只有这一种情况。且在java9中已经修复。在这里设计人员并没有尝试解决问题而是采取了绕过的方式。这样可能是一种更快速更安全的方法。
bug 6260652的数据库
关于此bug的更多解释

capacity相关方法及域

在看源码之前我一直以为ArrayList有一个名为capacity的域,而API文档的解释似乎也引导程序员有这样的想法。但是实际上在ArrayList中并没有这个域,相反ArrayList并不是通过 设定一个这样的值来实现管理capacity的。而是通过一系列方法。换句话说其实所谓的capacity更像是一种一次性的优化手段。主要目的仅仅在于创建数组的时候可以节省空间加快速度。因此只需要一次运行即可,如果只是运行一次就设定一个域显然有些浪费。
这里看一下有关的函数:

public void trimToSize()public void ensureCapacity(int minCapacity)private void ensureCapacityInternal(int minCapacity)private void ensureExplicitCapacity(int minCapacity)private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private void grow(int minCapacity)private static int hugeCapacity(int minCapacity)

接下来依次看看这几个函数:

截取

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

这个函数其实跟capacity并没有生关系。只是根据size的大小来截断数组,创建一个更小的数组。同样这里做了一些稍许的优化如果size为0的时候指向EMPTY_ELEMENTDATA。

在ArrayListList中大量的运用了三目运算。主要原因是ArrayList的构造是否指定了capacity以及元素数组是否为空。当然可以使用if等条件语句来执行,但是三目运算一个最大的好处是有返回值。使用三目运算使得程序显得更加紧凑。

ensureCapacity

    public void ensureCapacity(int minCapacity) {        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)            // any size if not default element table            ? 0            // larger than default for default empty table. It's already            // supposed to be at default size.            : DEFAULT_CAPACITY;        if (minCapacity > minExpand) {            ensureExplicitCapacity(minCapacity);        }    }    private void ensureCapacityInternal(int minCapacity) {        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity);    }    private void ensureExplicitCapacity(int minCapacity) {        modCount++;        // overflow-conscious code        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }

这三个方法很有意思。其实完成的是同一项功能但是却分了三段。其实最终完成确认工作的是ensureExplicitCapacity方法。前面两个方法相当于是两个适配器。分别对应于public的方法和private的方法。下面我来尝试分析一下为何是这样做,但内容仅仅是我的个人的猜测。

  1. 编程人员希望将public和private的调用分离。因为public的确认和private的确认目的不一样的。
    • 对于public的确认,首先java为程序员提供了一种指定capacity的权利。但是由于java提供了默认的capacity的大小。这虽然有时提供了方便但是未必一定是每时每刻都合理的,比如说有时用户并不需要这么大的数组,所以他使用ensureCapacity来指定一自己的大小。这时如果直接将其与DEFAULT_CAPACITY比较则是赋予编程人员指定capacity的权利就没有意义了。就会出现最小都创建DEFAULT_CAPACITY大小的数组,会造成空间上的浪费。这个方法的目的是给程序员更大的自由性,以及用于初始化的工作。
    • 对于private的情况,这种情况是适用于在增加元素或者序列化的时候。你可以看到ensureCapacityInternal实际上通常只在add类型的方法中调用。这个函数的目的是当ArrayList在使用默认的capacity的时候需要判断一下需要增加的值和默认的capacity的值DEFAULT_CAPACITY哪个大。否则指定DEFAULT_CAPACITY就没有意义。这个方法的目的是为了在默认情况下发挥DEFAULT_CAPACITY的功效。
  2. 设计人员希望将ensureExplicitCapacity作为一种增加modCount的方法。其实我对有一些疑问。这里仅仅抛出一些看法,以后可能需要更多的思考。
    • 首先为何将这个方法作为一种增加modCount的方法我个人认为是欠妥的。因为我理解的modCount方法是结构性修改的情况下做出的。但是在这里未必进了结构性的修改,也许放入grow中是一种更加合理的选择。从下文中每次使用ensureCapacityInternal都注释了// Increments modCount来提示程序员也说明这里其实是略微欠妥的。
    • 我猜想这里之所以放入modCount可能是为保证编程的确定性。因为如果每次只在grow这样的结构性修改中才能增加modCount那编程人员无法判断modCount什么时候增加,所以从这个角度上说这里可能是为了增加编程的确定性,而故意曲解了结构性调整的概念。
    • 另一种可能的原因是结构性的调整不仅仅是变化了数组,由1.的观点可以看出其实ensureExplicitCapacity是用于add的方法的所有增加元素。而增加元素显然是属于结构性调整的。因此将modCount放到这里而在add方法中不再增加modCount,这样从结果上说依然严格遵守了结构性调整的概念,所以这样来看也是非常合理的,虽然这只是通过不调用ensureExplicitCapacity来实现的。
  3. ensureExplicitCapacity是最终需要完成的任务。目的是判断是否需要对数组进扩展,而且每完成一次这样的查询则需要增加一次modCount而无论是否真的需要扩展。对于private的情形自然是每次完成都需要拓展。对于public的情形则需要根据其是否设定了capacity来给编程人员最大的自由度的同时提高便利性。

扩展数组

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;    private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        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);    }    private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0) // overflow            throw new OutOfMemoryError();        return (minCapacity > MAX_ARRAY_SIZE) ?            Integer.MAX_VALUE :            MAX_ARRAY_SIZE;

扩展数组的部分和之前的LinkedList的方法并没有流程上的区别,只是这里使用Arrays.copyOf来实现。
实现上也是通过每次增加当前数组的一半。对于minCapacity过大的情况需要实现满足miniCapacity大小的数组。对于miniCapacity过大的情况需要根据虚拟机的限制做出合理的设定。仅此。

增删改查

增删改查基本上没有特别需要注意的。但是这里的语法写得非常简洁,短小精干。还是有很多可以借鉴的地方。大量的应用了System.arraycopy方法。其余需要注意的也只有类型转换了移动数值的问题了。具体的程序细节看下源码就好。这里只集中看下其中几类的方法。

1.remove方法

public E remove(int index)public boolean remove(Object o)private void fastRemove(int index)

其实跟上面的ensureCapacity类似。最终做删除的是fastRemove,前两个都是不同情况下的适配器。而fastRemove是被抽取出来执行删除的公共方法。fastRemove不做检测也不返回值。而是直接删除。

    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    }

2.retainAll及batchRemove

另一类有意思的方法是batchRemove,这个方法实际上是retainAll调用的。

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

使用complement来判断当前元素是否包含集合内的元素。加入为true则只保留包含在Collection内的元素,反之易证。
创建两个变量一个指向执行前的变量pos,一个指向执行后的pos,这样可以做一个原地的算法不需要创建新数组。这里需要考虑的是抛出异常的情况。
如果r不等于size,则说明在运算的过程中抛出了异常,因此将未完成的运算即r之后的变量复制到当前运算的结果w之后,并且改变w的值,现在w的之后的值是多余的
如果w为size,则说明原数组没有被改动modified依然为false,如果w不为size,则说明modified为true,此外还说明数组w之后的元素为多余元素,将他们全部置为null,以方便GC回收。

3.clear
另外关于clear。并没有将指针指向空数组,而只是将原先所有的元素改为nulll。可能是设计人员认为程序员执行clear后任然会继续使用这个数组。如果指向空数组会在下次创建的时候浪费很多资源。

    public void clear() {        modCount++;        // clear to let GC do its work        for (int i = 0; i < size; i++)            elementData[i] = null;        size = 0;    }

copyOf和arraycopy的区别及分析

在最后我想说明一下两个常用的方法的区别,这是在上文中没有提到的:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType)public static native void arraycopy(Object src,  int  srcPos,                                        Object dest, int destPos,                                        int length);

首先第一个方法,copyOf是属于Arrays类的方法。从原型可以看出来这是一个将一个类型数组,而这里指定一个新的类型,这里还需要指定长度。方法会根据这个条件进行截断和补null。通常来说什么情况下会使用copyOf。是需要构造新的类型数组或者需要将一个数组类型转换的时候,比如ArrayList的构造器,toArray等方法,clone以及grow的时候。因为这个时候可能会涉及到类型变化。
而arraycopy是一个native方法,这也是我第一次见到一个实体的native方法。根据网上的资料native方法指的是依赖于非java实现的,而是通过java调用其他语言实现的。常见的是c及c++编写车dll之类的库,然后java调用这些库来实现的。从java的源码中我们看不出来如何实现的。API文档解释的比较多,简单的说这里就是将src从srcPos复制到dest的destPos处,复制长度为length。如果类型不匹配,超出索引范围,或者src或dest为空都会抛出异常。有意思的是这里虽然是一个数组却使用的是Object。这个方法通常用来复制数组的一部分到另一个数组的一部分,或者数组内部位移使用。你可以看到add和remove等方法使用到了这个方法。

接下来看下第一个方法copyOf的实现:

    public static <T,U> 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;    }

由以上内容可以看到copyOf方法是需要调用。
关于这个方法的讨论有两个点:

  1. 为何要使用(Object)newType == (Object)Object[].class)
    之所以要进行类型转换是因为Class<Object>.classClass<? extends T[]>不是可以互相比较的类型,这句话甚至都不会被编译,如果需要相互比较则需要使得一方是另一方的子类才可以。而java的通配符使得这个问题更加的复杂。因为虽然Object[]是其他所有类型数组的超类(这里有一定疑问stackoverflow原文是这样说的,暂时没查到。),但Class<Object[]>并不是Class<? extends T[]>的超类。所有需要将其都转换成为Object来判断。另外一种可以比较的方法是(newType == (Class<? extends Object[]>)Object[].class)判断是否可以将其转换成为Object。
  2. 为何不可以都是用反射的方法创建数组,一个理由是反射比较慢,而用普通的方法比较快,因此这三目运算实际上是为了做一些微小的优化。
    更多的解释请参看原文Arrays.copyOf实现的讨论

toArray

    public <T> T[] toArray(T[] a) {        if (a.length < size)            // Make a new array of a's runtime type, but my contents:            return (T[]) Arrays.copyOf(elementData, size, a.getClass());        System.arraycopy(elementData, 0, a, 0, size);        if (a.length > size)            a[size] = null;        return a;    }

对于输入的数组小于size的需要重新构造一个新的T[]符合size大小的数组,而输入数组大于size的需要将目前的elementData复制到指定的size中,如果a有多余的大小,则将下一个元素赋为null。
需要说明的是不可以想当然的将elementData给出去,这样会导致外部引用到的没有访问权限的数据的引用。

一个问题是这里Arrays.copyOf应该是已经返回的已经是T[]类型的了为何需要加(T[])?一种可能的原因是java本身泛型的限制。用a.getClass得到类并不是调用者希望的,这里得到的a的类型可能会更具体,因此这里添加一个转换,转换成T的类型(可能是更普遍更上层的类型)。

SubList

ArrayList可以通过subList的方法返回一个SubList的内部类,它同ArrayList一样也是继承自AbstractList并且实现了RandomAccess的。
ArrayList有几个内部私有final域:

  • private final AbstractList parent;
  • private final int parentOffset;
  • private final int offset;
  • int size;

和ArrayList的区别就是带有一个AbstractList接口的内部类,并且再ArrayList调用subList的时候将这个ArrayList付给parent域,并根据设定的起始目录和终止目录设定另外三个域。

        SubList(AbstractList<E> parent,                int offset, int fromIndex, int toIndex) {            this.parent = parent;            this.parentOffset = fromIndex;            this.offset = offset + fromIndex;            this.size = toIndex - fromIndex;            this.modCount = ArrayList.this.modCount;        }

SubList的其他方法并没有什么特别要说的,基本是ArrayList的翻版,只是需要注意考虑偏置。
需要考虑的SubList所用的iterator时,构造了一个新的内部匿名类,因为原先的类没办法实现偏置的调整。

2 0
原创粉丝点击