前言

    今天我们来研究一下Util包下的ArrayList类,及其相关的线程安全实现类,具体包括Vector、CopyOnWriteArrayList和集合工具类Collections提供的synchronizedList。
      首先我们知道ArrayList是非线程安全的,而在同一个包下的Vector则是ArrayList的线程安全实现的版本,同时为了优化线程安全下的ArrayList的性能,在java.util.concurrent包中又提供了基于写时复制的CopyOnWriteArrayList,在集合工具类Collections中提供了线程安全的List的支持。
      首先我们来研究一下最为基础的ArrayList类,在整个集合框架中,ArrayList都是使用频率非常高,并且支持多种使用场景的集合类。说到ArrayList,我们一定要先来了解一下数组Array及两者之间的关系。
      数组这一种数据结构,可以算是我们学习的所有数据结构中最为基础和重要的一种。在逻辑结构上,数组以元素加入的顺序组织数据;在物理结构上,数组是一段连续的内存空间;在操作上,数组支持O(1)的时间复杂度内查找和定位到元素,但是在(随机)插入和删除元素的操作上会花费更多时间,并常常需要移动大量的元素来完成插入和删除操作。同时由于数组的长度在初始化时就确定了,因此数组的长度也成为了其使用上的缺点,即数组的长度不能根据实际需求进行变化。因此基于这些问题,我们要使用数组的优势,同时规避和解决其缺点,在Java中就设计和实现了ArrayList类。
      ArrayList的实现是基于数组所实现的,因此在插入和删除操作上依旧会比较麻烦,性能弱于基于链表的LinkedList。但是在查找的操作上可以达到O(1)的时间复杂度,同时支持自动的扩容,能够解决数组的部分问题。

ArrayList的数据结构分析

//默认初始大小
    private static final int DEFAULT_CAPACITY = 10;
    //ArrayList实例为空时,使用该对象引用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //当ArrayList实例初始为空时使用,当ArrayList进行扩容时,进行评估
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //元素存储数组
    transient Object[] elementData; // non-private to simplify nested class access
    //当前ArrayList的大小
    private int size;

      如上所示,ArrayList的数据结构相对较为简单,定义了初始容量和一个共享的空数组引用,此处出现了两个空数组引用EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,关于两者的区别,将在后文的核心函数分析中做解析,请耐心研读下去。其中elementData数组即为核心的存储数组,作为元素的存储,同时需要注意该数组对象使用transient关键字进行修饰,保证了ArrayList在序列化时,该数组对象不会被序列化,从而保证了ArrayList的安全性。

ArrayList核心方法分析

      ArrayList的方法多较为简单,都是对数组的简单操作。我们简单的来看几个函数,在后文中,着重的对ArrayList的线程安全实现类做分析。在ArrayList中,比较麻烦的是删除(remove)操作,需要进行移位,同时ArrayList提供了很好的扩容优化机制,我们都将在本节中做详细的分析。

1)构造函数

public ArrayList(int initialCapacity) {
        //对传入参数进行检测,根据不同的参数值进行处理
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        }
        //当初始参数值等于0时,将对象引用直接指向共享独享EMPTY_ELEMENTDATA 
        else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    public ArrayList() {
        //无参构造函数,直接将对象引用指向共享空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

    }

      这个函数没有什么需要多说的,通过传入参数,对数组进行构造,其中需要注意的是,当传入的容量值initialCapacity为0时,则使用共享对象Empty_ELEMENTDATA的引用。当使用默认的无参构造函数时,可以看到使用的DEFAULTCAPACITY_EMPTY_ELEMENTDATA对象的引用。后文的扩容机制中,将继续描述二者的使用情况和区别

2)remove函数

public E remove(int index) {
        //同样进行传入参数范围检测
        rangeCheck(index);

        //删除操作造成ArrayList发生结构变化
        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;
    }

      如上代码所示,对于移位操作,在Java中并没有使用for循环进行移位,而是使用了系统System提供的数组拷贝函数arraycopy来完成的,在同一个数组中完成拷贝。

3)add函数以及ArrayList的扩容机制

      这一组的函数较多,我们首先来看一看add函数:

public boolean add(E e) {
        //确保当前数组的大小能够添加新元素
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        //检测当前元素数组是否初始化
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //若当前元素数组未初始化,则当前元素数组大小默认为DEFAULT_CAPACITY
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //对当前需要的数组大小进行检测
        ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 若需要的元素大小 大于 当前元素数组的大小 则进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

      如上代码所示,整个函数的逻辑是非常简单的,但是在Java8中为了很好的设计,每个函数都对应设计了非常简单的工作,同时可以在多个逻辑中被调用,很好的保证了函数的高内聚低耦合的特点。由函数ensureCapacityInternal提供当前所需要的元素数组大小,再由ensureExplicitCapacity决定是否对元素数组进行扩容。

      接下来我们再来看看最核心的扩容函数grow:

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;
        //如果需要进行扩容,则进行长度检测,是否需要达到数组的最大长度,即Int的最大表示范围
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //通过工具类Arrays提供的copyOf进行新数组的构建
        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;
    }

      整个逻辑还是非常的简单清楚,除此之外,ArrayList在提供扩容时,还提供了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);
        }
    }

      当我们初始化ArrayList的时候不确定需要多大的容量时,我们会使用无参的构造函数,这个时候会造成ArrayList不断的进行扩容,从而降低了效率,此时我们可以使用ensureCapacity来优化此过程,当我们在具体执行ArrayList的添加操作时,知道需要多少容量时,则可以使用该函数,来设置ArrayList的大小,从而不会多次的在Add函数中进行扩容的调用。
   具体函数的逻辑大致如下:首先该函数会对元素数组的引用进行检测,检测其是否是对DEFAULTCAPACITY_EMPTY_ELEMENTDATA的引用,即是否为默认的无参构造函数所构建的空数组引用,若是则扩容值为0,不是则使用默认的ArrayList的大小。
      到目前为止,关于DEFAULTCAPACITY_EMPTY_ELEMENTDATA和Empty_ELEMENTDATA的区别也非常明显了,前者为无参构造函数所使用的共享空数组,该数组在多个函数中用来判断当前元素数组为默认空数组,同时可以用来对是否进行扩容做判断。后者则是用户传入容量为0时所使用的共享空数组引用。

后言

      当分析完ArrayList的一些核心函数后,发现篇幅不够用了,那么关于ArrayList的线程安全实现类将在下一篇博客中进行详细分析。
      而回到ArrayList中来,可以看到虽然这个类非常的重要,但是有关它的具体实现又非常的简单,最核心的部分就集中在其扩容机制上面。通过分析ArrayList的源码,可以看出Java8的设计非常有讲究,虽然一些函数的功能非常的简单,依旧的被独立出来,保证函数的复用性和高内聚。
      最后简单的闲聊闲聊,对于Java8的源码分析,我们不仅可以提高对于Java的一些核心类的理解,在实际工程的使用中,能够更清楚选择哪一个类,知道不同类之间的区别。同时在分析中,我们可以学习Java官方对于一些问题是如何解决的,如何去优化一些存在的问题,如何不断的改善类的设计,从而达到更好的性能。这些东西都可以用到我们自己的实际开发中,不断的完善我们自己的开发水平,从而得到收获,因此希望大家可以更多的关注Java源码,关注底层设计。