[JDK8] ArrayList源码解析

类图结构

ArrayList是对List列表数据结构的一种具体实现,先放一张源码类图结构,有个直观的印象,该图是Java集合Collection类图的一个子集:
ArrayList源码结构

存储结构

transient Object[] elementData;

ArrayList实例本身只是一个普通的Java对象,它的内部封装了一个数组,添加到ArrayList里面的对象元素都是存储在这个数组当中。

数组存储结构的最大特点就是内存空间具有连续性,随机访问数组任何位置,时间复杂度都是O(1)

因此,单从数组存储结构,就能看出ArrayList的特点:

1、优秀的对象查找速度,时间复杂度永远是O(1)
2、增删对象的时候,涉及数组其它对象的前后移动,因此效率较低
3、在列表尾部的增删效率高于在头部的增删效率,因为尾部增删需要移动的其它对象较少

数组还有一个特点,就是一旦创建,数组长度就是固定的。当数组存储空间用完,还要继续向列表添加元素的时候,就需要开辟新的存储空间,这就是ArrayList的数组扩容机制,后文再讲。

ArrayList初始化

ArrayList有三个构造方法:

// 对象存储数组
transient Object[] elementData;

// 两个空集合标识,一个表示“人为指定”,一个表示“系统默认”
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 无参构造函数
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 构造的时候,指定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);
    }
}

// 构造的时候,添加一些初始化元素
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;
    }
}

从源码可以看出,构造ArrayList的时候,最关心的是就数组elementData存储空间的初始化。

通过以下方式构造ArrayList时,数组暂不分配存储空间:

ArrayList list = new ArrayList();
ArrayList list = new ArrayList(0);
ArrayList list = new ArrayList(list);// list是空的

ArrayList构造完成以后,数组变量elementData会指向一个预定义的空数组对象,要么是EMPTY_ELEMENTDATA,要么是DEFAULTCAPACITY_EMPTY_ELEMENTDATA

为什么要预定义两个空数组对象呢?

这是在为后面新增元素时数组扩容作准备,数组第一次扩容时,需要知道指向空对象的原因是“人为指定”还是“系统默认”。暂时先记住这一点,后面讲扩容时再具体分析。

通过以下方式构造ArrayList时,数组立即分配存储空间:

ArrayList list = new ArrayList(128);
ArrayList list = new ArrayList(list);// list里面有对象元素

小结:构建ArrayList对象时,为了优化性能,非必要的情况下,不会分配数组存储空间,如果明确知道后续操作需要多大的数组空间,指定一个合适的初始容量也是极好的。

新增对象与数组扩容

新增元素的方法有四个,实现上大同小异,顺着其中任何一个方法追踪下去,很快就可以看到新增逻辑和数组扩容机制,数组扩容只在新增的时候才会有。

add(E e)方法为例,查看完整方法调用链如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算数组存储空间的最小长度
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// 数组扩容的核心逻辑
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);
}

这段代码逻辑包含两个方面内容:添加元素、数组扩容。

添加元素的逻辑是这样的:

  1. 判断当前的数组空间够不够用
  2. 如果够用,将元素添加到数组当中
  3. 如果不够用,先触发数组扩容机制,再将元素添加到数组当中

数组扩容的逻辑是这样的:

  1. 数组空间不足时才会触发扩容机制,创建新的内存数组,长度是原来的1.5倍,将原数组对象复制到新数组,elementData对象引用指向新数组
  2. 第一次扩容时,数组分配长度取决于构造ArrayList时的参数,还记得那两个空数组对象么?
  3. 如果初始化ArrayList时,空数组对象是“系统默认”的,那么,数组扩容第一次得到的内存空间就是10个对象长度
  4. 如果初始化ArrayList时,空数组对象是“人为指定”的,那么,数组扩容第一次得到的内存空间就是1个对象长度
  5. 数组扩容的最大值是Integer.MAX_VALUE,再继续扩容就会抛出OutOfMemoryError异常

扩容机制的好处是可以保证对象元素存储空间的动态增加,避开了数组固定长度的限制,但这也是降低列表性能的操作。

因此,在实际应用场景下,如何降低扩容次数也是ArrayList一个可以考虑的优化方向。

删除对象

删除方法有多个,实现也是大同小异,最常用的删除操作是:

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

删除逻辑非常简单:

  1. 检查删除对象索引是否有效,索引就对应数组下标
  2. 拿到将要删除的对象
  3. 将数组删除位置之后的所有对象前移一位
  4. 返回删除对象

需要注意的是,ArrayList只有数组的扩容机制,没有“减容机制”!删除元素的时候不会动态减少数组空间。

面试的时候,不止一次的有面试者告诉我:ArrayList数组空间是动态分配的,新增对象时,空间不够就增加,删除对象时,空间多了就减少。这完全是错误的理解!

再强调一次:ArrayList底层数组只会在新增元素且数组空间不足时扩容,数组空间没有动态变小的途径!!!

查找对象

ArrayList里面查找对象非常的快,因为数组具有时间复杂度为O(1)的随机查找能力。

查找源码如下:

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

逻辑也简单,没啥可分析的:

  1. 检查查找索引是否有效
  2. 从数组中获取对象并返回

线程安全性

ArrayList是线程不安全的,因为源码里面没有涉及到任何的锁操作,也没有任何的数据同步保障。

所以,多线程场景下使用ArrayList存在线程安全问题。

如何解决这个问题呢?提供三个方案。

Vector

Vector实现逻辑与ArrayList很像,最大的区别在于Vector会在方法上使用synchronzied关键字保证线程安全:

public synchronized boolean add(E e) {...}
public synchronized E remove(int index) {...}
public synchronized E get(int index) {...}

可以看到,新增、删除、查找,这三类方法都加上了synchronized关键字。

Vector保证了线程安全,但牺牲了增删查的效率,尤其是查找效率大打折扣,这是非常致命的一点。

再提一个它们的区别,默认情况下,ArrayList的扩容因子是1.5Vector的扩容因子是2。也就是它们各自的数组扩容速度,相同数据量下,Vector扩容次数不会高于ArrayList

因此,小数据量的场景下,即使Vector有同步操作,它的新增速度通常也会优于ArrayList,大数据量的场景下,Vector通常又会比ArrayList浪费更多的数组存储空间。

总有人跟我说,不要使用Vector,因为它的同步性能低下。

我不否认这一点,但是我想说的是,Vector并非一无是处,它也有优于ArrayList的场景,合理的选择利用它们,扬长避短,才是编程取舍之道。

SynchronizedList

SynchronizedList是集合工具类Collections里面的一个静态内部类,通常,用法如下:

ArrayList<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("string");
String str = synchronizedList.get(0);

SynchronizedList是保证线程安全的方法也是利用的synchronized同步机制:

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}
public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}

这种方案是运用了代理模式,对List实现类进行了代理,在增删查操作之前添加同步操作,效率也不高。

CopyOnWriteArrayList

使用List集合的业务场景,通常情况下是读多写少,CopyOnWriteArrayList就是专门为这种业务场景设计的。

它的特点是:

  1. 读操作支持并发,写操作保证同步
  2. 写操作进行时,不会阻塞读操作
  3. 它能保证数据不出错,但是并非严格意义上的线程安全

看看新增和查找的源码,从中可以看出它的实现原理:

// 数组存储结构
private transient volatile Object[] array;

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
public E get(int index) {
    return get(getArray(), index);
}

它的写操作逻辑是这样的:

  1. 底层还是数组存储结构
  2. 进行写操作前,将原数组复制一份,新数组空间长度加1
  3. 在新数组中进行写操作
  4. 写操作完成后,将原数组引用指向新数组即可
  5. 并发写操作时加锁,保证同步

从写操作逻辑中,可以看出CopyOnWriteArrayList为什么会对读操作有很好的并发支持。

读操作包括新增和删除,每一次写操作,都需要复制一次数组,对内存空间有一定程度的浪费。

而且,因为读写之间没有同步机制,所以写操作成功后,不一定能及时反馈给读操作,可能就会出现两种现象:

  1. 对象新增后不能及时读到
  2. 对象删除后还能读到

这就是上面说的,从严格意义上讲,CopyOnWriteArrayList并不是线程安全的,但是宏观上,它又能保证数据的正确性,很有特点的一个类!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章