ArrayList源代码分析

来源:互联网 发布:淘宝客服分流时间设置 编辑:程序博客网 时间:2024/06/08 08:29

本源代码来自JDK1.8  与1.7、1.6 略有不同

1 ArrayList中的属性

1 初始容量

初始大小为10

/**     * Shared empty array instance used for empty instances.     */    private static final Object[] EMPTY_ELEMENTDATA = {};

2 空的Object数组

当初始化容量为0时,就构造这样一个空的Objcet类型数组。

/**     * Shared empty array instance used for empty instances.     */    private static final Object[] EMPTY_ELEMENTDATA = {};

3 默认的空的Object数组

根据注释,这个大概意思就是构造一个空的对象数组,用来与EMPTY_ELEMENTDATA 这个数组进行对比,来确定当第一次向ArrayList中添加数据时,应该如果进行扩容,就是增加多大的容量暂时还不太好理解这个,往后看就懂了

/**     * 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.     */    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

4 真正用于保存数组的对象数组

ArrayList中 用来存储数组的对象数组 这个对象是transient 的,即不可被序列化的

由此可见,ArrayList底层实际是在维护一个对象数组

 /**     * The array buffer into which the elements of the ArrayList are stored.     * The capacity of the ArrayList is the length of this array buffer. Any     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA     * will be expanded to DEFAULT_CAPACITY when the first element is added.     */    transient Object[] elementData; // non-private to simplify nested class access

5 ArrayList 大小Size

用于标识当前ArrayList的真实大小

 /**     * The size of the ArrayList (the number of elements it contains).     *     * @serial     */    private int size;

2 构造方法

ArrayList 共有三个构造方法

1 默认的空的构造方法

根据这个方法的注释,默认的构造方法构造的是构造了一个容量为十的List,但是从源代码来看,实际上在这个地方只是构造了一个空的对象数组,那么为什么说是十呢,在后来就能看懂了。

/**     * Constructs an empty list with an initial capacity of ten.     */    public ArrayList() {        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    }

2 带有初始化容量的构造方法

根据指定的容量构造一个空的对象数组。如果容量为负数,抛出异常。应该注意的是,当指定容量为0时,构造时使用的就是前面的全局变量,即为一个空的对象数组。

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

3 利用一个已有的Collection来构造

这里首先用到了Collection.toArray()方法进行数组转换。

在这里有一个确认对象类型的检验,如果集合中的对象不是Objec类型,利用Array类的静态方法进行数组的拷贝,这里给出的注释是在调用toArray()方法时可能存在问题,这里的6260652应该BUG的Id号

当转换的数组为空时,ArryaList并没有使用转换的空结果,而是依然使用自己构造的空数组。也许Java程序员认为外来的都是不可信任的吧(个人理解)

<span style="font-size:18px;">public ArrayList(Collection<? extends E> c) {</span>        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;        }    }


3 Add方法

这里把Add方法单独拿出来,这是因为这个Add不仅仅是一个方法,它是一个流程。如果把这个流程理解了,那么整个ArrayList也就懂了。前面遗留的几个不懂得地方在这都有解答

Add方法有两个

(1)Add(E e)

可以看到这个方法永远返回true,所以不应该通过add的返回值来判断是否添加成功。换句话说,Java的大神们在告诉你,我的方法永远都不会出错,你要相信我

public boolean add(E e) {        ensureCapacityInternal(size + 1);  // Increments modCount!!        elementData[size++] = e;        return true;    }
这个方法一共只有三行,其中第一行的才是关键。这里面是在确定ArrayList的容量能否再多加一个。我们看到ArrayList是个无底洞对吧?想放多少放多,实际上都是源于这句话。而这句话的方法在做什么呢?下面给出源代码

private void ensureCapacityInternal(int minCapacity) {        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity);    }

ensureCapacityInternal这个方法首先判断了当前的对象数组是不是默认的空数组,如果是的话,那么就在默认容量(10)和需要的容量中取一个最大值,然后把得到的这个值传递给下一个函数ensureExplicitCapacity。

从这个地方就可以解答了前面的疑问了,如果使用默认的构造方法,那么elementData 就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,当首次插入对象时,取最大值的时候一定是10,再经过后面的扩容,实际上就相当于构造了一个长度为10的对象数组。

private void ensureExplicitCapacity(int minCapacity) {        modCount++;        // overflow-conscious code        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }
ensureExplicitCapacity这个方法第一行先不管是干嘛的,看后面的代码,很简单吧! 就是判断需要的容量和现在的用来存储的对象的数组(elementData)那个更大,如果现在已有的不够大了,那就扩容呗! grow(minCapacity)

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



这段代码进行了扩容,可以看到扩容通过newCapacity = oldCapacity + (oldCapacity >> 1);这段代码实现,实际上就是将新的容量变为原来容量的1.5倍

但是需要对扩容后的容量进行检验。共有两种可能,

1得到的容量比需要的容量小,为什么会有这种情况呢,溢出了呗。

2得到的容量超过了规定的最大容量,进入hugeCapacity中,如果需要的容量小于0,抛出内存溢出异常,如果需要的容量比规定的最大容量大,那么最大容量只能是 Integer.MAX_VALUE

最后将elementData 通过Array的复制拷贝方法进行了扩容。


由此就完成了整个添加新的元素的过程。从这个过程可以看出,ArrayList也并不是无限大的,它指定了一个最大容量 是Integer.MAX_VALUE - 8,实际最大只能是Integer.MAX_VALUE这个值了。

(2)add(int index, E element)

这个方法是在指定的位置插入元素 element

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++;    }
第一行没什么好说的,肯定是检验index是否合法
private void rangeCheckForAdd(int index) {        if (index > size || index < 0)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    }
第二行与前面add方法一致,检验容量并扩容

第三行是arraycopy的方法,在这里面实现的就是将原来的数组从index位置开始,每个元素向后移一位。这是一个本地方法,看不到源代码啦

第四行比较简单啦,就是把要插入的元素放在他应该出现的位置上。

4Remove 方法

(1) remove(int index)

从指定的位置删除元素。实现原理就是把index后面的数据都向前移位。O(n)

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

private void rangeCheck(int index) {        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    }
(1)第一行就是检验index的返回,有人会问为什么不检验负数的情况。本人亲自试验了一下,如果index是负数的话,第一行是可以正确执行的,但是在elementData(index)这样地方就会抛出java.lang.ArrayIndexOutOfBoundsException: -1即 数组越界异常其实个人觉得,如果没有第一句 检验,仍然会抛出数组越界异常。只不过多了第一行的话,抛出的异常就是java.lang.IndexOutOfBoundsException: Index: 2, Size: 2,而且会打印出错误信息。又有哪个Java程序员不喜欢看到更详细的异常信息呢?

(2) 计算移动量,如果要删除的元素不是最后一位,就要进行移动。

(3)将移动后的数组末尾赋值null。这里有一句注释,让GC去回收。这是一种释放内存的方式。但是我们不能依赖这种方法, 因为我们永远不知道GC如何能去回收它。

(2)remove(Object o)

删除指定的元素,仅删除符合条件的第一个元素,如果不存在,返回false 即删除失败

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

分析这段代码,首先对o进行了非空处理。如果不进行这样的处理,将会引发o.equals的空指针异常。这个是非常常见的处理方式

在这if else语句块中,都用了fastRemove(index) 听上去挺高大上的,快速删除的,其实看了源码就知道,很简单。

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)方法的后半部门吗?为何不直接调用这个方法呢?肯定是为了提交一点点效率吗,这毕竟是Java API嘛,要是不能高效一点,是吧、、、


(3)removeAll(Collection<?> c)

在集合中,删除与Collection中元素相等的元素。

public boolean removeAll(Collection<?> c) {        Objects.requireNonNull(c);        return batchRemove(c, false);    }

第一行是java1.7新出的工具类,用于检验c非空的,如果为空,抛出空指针异常

接下来就是batchRemove(c, false) 这个方法了。源码里虽然没有给出注释,但从第一个if就可以看出,这个方法是用于返回与给定c相同或不同的元素,而且是全部。

 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;    }
下面来分析一下这段代码。首先对集合中的对象数组进行了一次复制,然后将数组中的元素依次与collection进行对比。try{}块的结果就是得到了一个数组,数组前w位元素或与collection相同(complement=true的时候)或不同(complement=false)

下面来看finally,先来看第二个if{}代码块,它的作用就是把数组中w以后的元素全部变为null值,让gc来回收。

现在回到finally的第一个if中,看条件(r != size),似乎永远不会满足这个条件吧。上面的for循环一直r++啊,可是别忘了,c.contains(elementData[r])这句话是有可能抛出异常的,如果一旦类型不匹配,就会抛出异常 进入finally中。

这个方法,如果没有删除任何数据,那么将会返回false。


现在我们再来看return batchRemove(c, false);这行的含义吧。它指定了第二个参数为false,所以在batchRemove()保留下来的都是与collection中不同的元素。相同的元素都被删除了。这样就达到了removeAll(Collection<?> c)的目的。

5 迭代器

迭代器可谓是操作ArrayList的神器,在ArrayList内部通过内部类的形式实现了Iterator接口,完成对ArrayList的操作。先让我们来内部一 Itr的源码

private class Itr implements Iterator<E> {        int cursor;       // index of next element to return        int lastRet = -1; // index of last element returned; -1 if no such        int expectedModCount = modCount;        public boolean hasNext() {            return cursor != size;        }        @SuppressWarnings("unchecked")        public E next() {            checkForComodification();            int i = cursor;            if (i >= size)                throw new NoSuchElementException();            Object[] elementData = ArrayList.this.elementData;            if (i >= elementData.length)                throw new ConcurrentModificationException();            cursor = i + 1;            return (E) elementData[lastRet = i];        }        public void remove() {            if (lastRet < 0)                throw new IllegalStateException();            checkForComodification();            try {                ArrayList.this.remove(lastRet);                cursor = lastRet;                lastRet = -1;                expectedModCount = modCount;            } catch (IndexOutOfBoundsException ex) {                throw new ConcurrentModificationException();            }        }        @Override        @SuppressWarnings("unchecked")        public void forEachRemaining(Consumer<? super E> consumer) {            Objects.requireNonNull(consumer);            final int size = ArrayList.this.size;            int i = cursor;            if (i >= size) {                return;            }            final Object[] elementData = ArrayList.this.elementData;            if (i >= elementData.length) {                throw new ConcurrentModificationException();            }            while (i != size && modCount == expectedModCount) {                consumer.accept((E) elementData[i++]);            }            // update once at end of iteration to reduce heap write traffic            cursor = i;            lastRet = i - 1;            checkForComodification();        }        final void checkForComodification() {            if (modCount != expectedModCount)                throw new ConcurrentModificationException();        }    }


这个类并不长,让我们来分析一下

(1) 属性

a  游标

int cursor; 我们知道iterator是始终向前走的,就是这个游标始终在++的原因

b 末尾标识

 int lastRe 标识了最后一个返回的元素的索引位置,-1代表这个元素不存在

C 操作数标识

 int expectedModCount = modCount;  这个非常重要,它用来校验在使用iterator期间,是否存在非Iterator的操作对ArrayList进行了修改。

(2)checkForComodification 方法

这个方法很简单,但是可以看到在这个内部类中,几乎所有方法都在调用它。它所做的工作就是校验在使用iterator期间,是否存在非Iterator的操作对ArrayList进行了修改。

在ArrayList中,有很多操作都修改了一个变量,modCount。每次进行操作,modCount都在++。前面一直不理解这个有什么用,在这看到了它的用意。Iterator的游标特性决定了它对ArrayList中元素在这一时刻的位置很敏感,如果当前游标在index位置,而有其他操作在index-1的位置上插入了一个元素,那么调用iterator的next()方法,返回的还是当前这个元素,这样就乱了。为了避免这个情况发生,需要在这个期间把ArrayList“锁住“。它并没有实现真正的锁,所以采用了这个校验的方式。

(3)next()

返回当前游标位置的元素,这里面第一个if判断的条件很有意思。因此游标前移,当移动到一个不存在数据的地方,它抛出了异常。而并没有返回null。这就是我们为什么在使用iterator的时候不能用(null==iterator.next())来判断的原因。而是在要每次循环开始的时候判断iterator.hasNext()。

(4) remove()

删除lastRe 所标识位置的元素。我们可以把它理解为当前元素。在try前面有一个校验,保证元素没有被改动过,要不就删错了。

在try{}语句块中,首先删除了lastRe标识的元素,然后让游标指向了这个位置。我们知道在删除元素以后,这个位置有了新的元素,这样再次调用next()的时候不会出现空指针异常,更不会跳过一个元素。

最后expectedModCount = modCount;这句相当于释放了锁。也是在表示,在我Iterator的地盘,只有我能够去修改mod,别人动了就不行!

6 set 与get


set将指定位置的元素替换

public E set(int index, E element) {        rangeCheck(index);        E oldValue = elementData(index);        elementData[index] = element;        return oldValue;    }
get  获取指定位置的的元素

 public E get(int index) {        rangeCheck(index);        return elementData(index);    }

private void rangeCheck(int index) {        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    }

这两个方法里面  都有一步rangeCheck,即检验输入索引位置是否越界,它将会引发越界异常。

7 contains 、indexOf、lastIndexOf

这三个方法功能很相近

第一个是查看ArrayList中是否含有指定元素

第二个是返回指定元素的第一个出现的索引位置

第三个返回指定元素的最后一个出现的索引位置

这是一组依赖的方法,实际上执行的是同一段代码。

public boolean contains(Object o) {        return indexOf(o) >= 0;    }

public int indexOf(Object o) {        if (o == null) {            for (int i = 0; i < size; i++)                if (elementData[i]==null)                    return i;        } else {            for (int i = 0; i < size; i++)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }
 public int lastIndexOf(Object o) {        if (o == null) {            for (int i = size-1; i >= 0; i--)                if (elementData[i]==null)                    return i;        } else {            for (int i = size-1; i >= 0; i--)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }


这段代码很简单,就是一个简单的遍历,只是变量的起点不同而已,就实现了不同的功能。indexof的代码有没有觉得似曾相识呢?就是跟remove的方法差不多呢,这也再一次提醒我们,如果要使用一个对象的方法时,一定要注意解决空指针异常

8 clone

拷贝,重写了Objec类的拷贝方法,ArrayList的拷贝是浅拷贝

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

这里面用到了前面看到很多次的方法,Arrays.copyOf()

9 sort

排序方法,这个方法用到的是Array类的静态排序方法

public void sort(Comparator<? super E> c) {        final int expectedModCount = modCount;        Arrays.sort((E[]) elementData, 0, size, c);        if (modCount != expectedModCount) {            throw new ConcurrentModificationException();        }        modCount++;    }

10 总结

上面介绍了ArrayList中常用的方法,第一次较为认真地读源代码,确实发现了不少秘密。

(1) ArrayList是用Object数组来实现了,所谓的动态扩容,实际上就是在不断地产生新的数组,然后进行复制。

(2)在使用ArrayList插入大量元素时,应该有选择的申请一个容量,而不是使用默认的容量。扩容也是会消耗时间的

(3) ArrayList不是线程安全的,它的操作中没有使用同步方法。如果想使用线程安全的,可以用Vector,也可以使用Collections.synchronizedList来创建线程安全的list。






















0 0