讲讲ArrayList

在之前《Java中的Collection》文章中简单粗略的介绍了Java中Collection前世今生及常用的Collection,这篇文章我们就单独聊聊ArrayList—一个Java开发者开发过程中绕不开的数据结构。

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

通过这个可以查看到ArrayList继承了AbstractList并实现List、RandomAccess、Cloneable,Serializable等接口;通过这些接口我们大概知道了ArrayList拥有的基本的特性:
1. 必须保持元素特定的顺序(是一个List)
2. 支持快速(通常是固定时间)随机访问(RandomAccess)
3. 支持对象clone,是浅复制还是深复制,看是如何实现clone()方法(Cloneable)
4. 支持对象序列化(Serializable)

他是List

既然他是List,那么他就应该拥有List的一切特性—add/remove/iterator。
我们先看看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);
        }
    }

通过以上代码我们可以发现,当构造一个ArrayList对象时创建了一个initialCapacity大小的数组—一个elementData成员变量。通过这个构造方法我们不会得到太多的信息,我们只知道ArrayList创建了一个指定大小的数组。

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

上面就是List特性之一的add方法,他返回的结果永远都是true,另外执行了ensureCapacityInternal方法接着将e对象塞入elementData数组中。我们知道数组的大小不可变的,但是ArrayList不断的add对象进来,迟早会把elementData数组撑爆的,所以按照这个园里来推算,ArrayList是一个可变的”数组”,那么ensureCapacityInternal方法中应该进行了两个操作:
1. 判断elementData当前的容量是否足够
2. 按照扩容后大小迁移elementData中的数据

那么他是如何进行这些操作呢?看看下面的grow方法

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

从这个方法可以看出elementData新容量永远比老容量要高出1.5倍,扩容后进行copy操作创建新elementData,在这过程中JVM将elementData中的元素进行复制迁移到新的elementData中。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

grow是一个扩容、复制迁移数组的方法,那么ensureExplicitCapacity方法就是条件—判断是否需要执行grow方法。从条件判断语句我们可以清楚的看出,只要elementData.length比当前要求的最小容量大就足以跳过grow了,这个条件告诉我们设置按业务所需设置elementData的初始大小是非常重要,他可以间接的减少grow方法执行次数—例如你有一个数据流以20的倍数下发,这时候可以使用以20*0.75作为elementData的初始大小(具体根据业务情况而定)。

下面我们看看他的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;
    }

首先对index进行范围界定,该方法(rangeCheck)会抛出IndexOutOfBoundsException异常,然后通过elementData方法取出index位置上的对象。

 E elementData(int index) {
        return (E) elementData[index];
    }

这是简单不能再简单的取值方法了,所以remove前必须保证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

从以上代码我们可以大概知道他有以下意图:
1. 重组elementData数组(将index对应的value移除)
2. 将重组后的elementData最后一个元素设置为null

例如elementData中有5个元素,remove的index是3,所以numMoved为1,接着代码看就是显示为:

System.arraycopy(elementData, 4, elementData, 3, 1);

意思就是elementData从第4个位置(含)开始移动到elementData第三个位置,有效位置为1。通过 这样的一个copy操作就把需要移除的index对应的elementData元素给移除了(被以前第4个位置的元素替代了),但是我们发现elementData大小没变,并且新的elementData数组第3个元素和第4个元素是一样的,故需要将多余的重复的预算置为null。如下

   elementData[--size] = null; // clear to let GC do its work

ArrayList迭代器

   /**
     * An optimized version of AbstractList.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(Consumersuper 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();
        }
    }

上面就是ArrayList实现Iterator接口实现的next、forEachRemaining等特性。这篇文章我们也主要针对next方法进行探讨。

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

从上面next方法可以看出,诸多index的if判断语句并且这些判断语句都有抛出异常的可能,这对于应用或者系统来说这是致命的。因此在执行迭代器操作的过程中一定要保证elementData 数组的数据完整性(禁忌对elementData 数组进行增删改操作)[此处在应用的过程中出现的问题比较多]。那如何避免呢?
1. 在创建迭代器后,使用迭代器内部的增删改方法。
2.  使用Collections.synchronizedList 方法将该列表“包装”起来,达到同步效果。
3. 创建的ArrayList创建一个副本,clone出一个副本。

支持快速随机访问

我们可以打开RandomAccess这个接口发现,发现他是一个空接口,那么他有什么用呢?
答案是他肯定有用的,不然Java不会设计这么个接口,他一般给一些Collections工具提供了判断依据,例如Collections的shuffle方法。

   public static void shuffle(List list, Random rnd) {
        int size = list.size();
        if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=size; i>1; i--)
                swap(list, i-1, rnd.nextInt(i));
        } else {
            Object arr[] = list.toArray();

            // Shuffle array
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));

            // Dump array back into list
            // instead of using a raw type here, it's possible to capture
            // the wildcard but it will require a call to a supplementary
            // private method
            ListIterator it = list.listIterator();
            for (int i=0; i

可以看出,当你实现了RandomAccess时,他会调用另外一套代码块而达到区分。同理copy方法也是同样的道理。

    public static  void copy(Listsuper T> dest, List src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; ielse {
            ListIteratorsuper T> di=dest.listIterator();
            ListIterator si=src.listIterator();
            for (int i=0; i

另外我们可以发现ArrayList的兄弟类LinkedList没有实现RandomAccess,一个实现了一个没实现,在算法上使用肯定有区别的,具体我们在以后的文章细说。

支持对象clone

clone方法是Object的方法,我们都知道一切皆是对象,那么世界万物都是Object的子类,所以clone方法也就被传下来的了。那么仅仅重写clone()方法就可以达到复制效果么?

答案肯定是错误的。我们知道clone()是伴有CloneNotSupportedException异常的—Object的clone()方法是一个native方法,当你调用它的clone的方法,发现这个对象时不支持clone就会抛出异常,详细后面的文章细讲。

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

从上面代码可以看出,ArrayList的clone是一种浅拷贝,关于对象的拷贝方式(浅拷贝、深拷贝),在后面的文章细讲

对象序列化

这个话题比较大,可以单独用一篇文章讲解,这里我就简单说明下—看看ArrayList如何实现的。

一个对象序列化(serialize)肯定必然出现反序列化(deserialize),他们所对应的方法writeObject和readObject。

   private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);
        // Write out all elements in the proper order.
        for (int i=0; iif (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

上面是序列化方法,也就是Object to byte[]的过程,我们可以看出,他是先将size写入,然后依次转换数组中的各个Object。

   private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;
        // Read in size, and any hidden stuff
        s.defaultReadObject();
        // Read in capacity
        s.readInt(); // ignored
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);
            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i

上面是反序列化的方法,首先可以看出发序列化先确定size,然后通过size确定当前size的大小,然后再反序列化各个Object。并且谁先write ,就先read谁,write的什么样的类型,read的也就是什么样的类型。关于序列化,我们在后面的文章详细讲到。

从上面的代码中我们可以看出,在序列化的过程中,如果执行了elementData 数组的数据完整
性的操作,将会抛出异常,这和在迭代的过程中,不能进行操作是一致的。执行完序列化后,再进行增删改操作,接着反序列化,中途的操作将会被反序列化操作所覆盖。

你可能感兴趣的:(讲讲ArrayList)