浅谈ArrayList源码


以下源码分析基于 JDK 1.8。

1 、ArrayList概述

ArrayList 基于数组实现的,支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2、成员变量
/**
     * Default initial capacity.
     * 默认初始化容量 
     * 主要是给调用无参构造函数生成的对象进行初始化数组长度
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     * 用于初始化elementData的空数组 
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 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.
     * 默认容量下用于初始化elementData的空数组 
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 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

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     * 集合的大小
     */
    private int size;
    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     *  集合的最大容量
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

在使用有参构造函数实例化ArrayList的时候,如果指定数组为0,则使用EMPTY_ELEMENTDATA来初始化elementData;如果是使用无参构造函数实例化,则使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA来初始化elementData。并且无参构造函数实例化的ArrayList添加第一个元素后立即扩充成一个大小为10的数组。

3、构造函数

构造器分析
ArrayList拥有三个构造器,它们本质上都是在对内部的elementData进行初始化操作

无参构造器

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这是最简单的构造器,仅仅只是将elementData指向常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,以便于在添加第一个元素时,扩容至默认容量capacity = 10的大小

显式设置初始化容量的构造器

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

这个构造器首先需要保证手动指定的容量大于0,否则会抛出IllegalArgumentException异常;其次,判断手动指定的容量是否等于0,如果是,则直接将elementData指向EMPTY_ELEMENTDATA,如果不是,则创建一个跟指定容量相同大小的Object类型的数组作为elementData即可

基于另一个集合创建ArrayList的构造器

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

这个构造器相对比较复杂。首先,将参数集合转换为数组对象。接下来,判断新数组对象是否包含元素。如果没有元素,则直接将elementData指向EMPTY_ELEMENTDATA。如果数组对象包含元素,则要判定数组对象的类型是否属于Object数组类型(由于不同集合有不同的toArray方法的实现,所以toArray方法不一定返回的是Object类型的数组)。在类型不属于Object数组类型时,根据当前的数组对象的大小和实际元素,复制一个新的Object类型的数组作为elementData

4、动态扩容机制

当Arraylist进行add操作的时候判断是否需要进行扩容。

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 元素个数加一,并且确认数组长度是否足够 
        elementData[size++] = e;		//在列表最后一个元素后添加数据。
        return true;
    }

1. 首先会根据ensureCapacityInternal()方法获取到当前所需要的最小容量。

private void ensureCapacityInternal(int minCapacity) {  //minCapacity 指的是需要的最小数组长度
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
这里判断ArrayList的初始化是否通过无参构造函数进行初始化的,如果是无参则给ArrayList初始化一个长度为10的空数组(默认容量的大小:DEFAULT_CAPACITY)。所以最小的数组长度不一定是传过来的参数。

2. 接下来根据ensureExplicitCapacity(int minCapacity)方法来判断是否需要进行扩容。

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

minCapacity - elementData.length > 0:如果所需要的最小长度大于当前的数组长度就需要调用grow(int minCaoacity)方法进行扩容。

 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);  //把当前数组长度扩容1.5倍
        if (newCapacity - minCapacity < 0)  //如果所需最小的数组长度大于扩容1.5倍之后的数组则最后扩容的数组长度为所需的最小长度
            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.如果minCapacity大于原始容量的1.5倍:
最后扩容的数组长度为所需的最小长度即minCapacity。如用无参构造函数初始化的ArrayList第一次进行add操作

2.如果minCapacity小于原始容量的1.5倍:
ArrayList认为这个minCapacity的容量也是不安全的,很有可能会进行第二次扩容。为了减少扩容带来的消耗,此时ArrayList认为扩容后的容量应该为原始容量的1.5倍。如用无参构造函数初始化的ArrayList第十一次进行add操作。

3.扩容后的容量大于了集合允许的最大容量(MAX_ARRAY_SIZE)
集合的最大容量为Integer.MAX_VALUE — 8,这节省的8个容量是用于某些虚拟机在数组中保存一些头信息或描述信息。如果集合的容量实在不够,那只能把原本节省下来的8个容量拿来使用,这也意味着,在某些虚拟机环境下,这样的操作会导致OutOfMemoryError错误。由hugeCapacity(int minCapacity)进行完成该操作。

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

4.确定扩容后的容量后,根据这个容量创建一个新的数组,并把原始数据的数据拷贝过来

从以上看来扩容本身就是一种低效率的操作(除了要开辟新的内存空间外,还得把数据一个一个的复制到新的空间中),并且随着原始数据增加,操作的速度也会越来越慢因为copy的数据越来越多,所以最好能在构造ArrayList时就预估好容量的大小,避免扩容带来的开销。

5、ArrayList的序列化和反序列化

ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。

保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。

transient Object[] elementData; // non-private to simplify nested class access
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。

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; i++) {
            a[i] = s.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; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

首先通过s.defaultWriteObject();对非transient变量进行了序列化。然后又通过

for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

这个循环对数组中的有值的元素逐个进行了序列化操作。

反序列化时也是一样,首先通过s.defaultReadObject();;对非transient变量进行了反序列化。然后又通过

for (int i=0; i<size; i++) {
a[i] = s.readObject();
}

这个循环对数组中的有值的元素逐个进行反序列化操作。

序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);

6、删除操作

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 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;
}
8、怎么得到一个线程安全的ArrayList

可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List list = new ArrayList<>();
List synList = Collections.synchronizedList(list);
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List list = new CopyOnWriteArrayList<>();

7、CopyOnWriteArrayList
  1. 读写分离
    写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

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

final void setArray(Object[] a) {
    array = a;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
  1. 适用场景
    CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

但是 CopyOnWriteArrayList 有其缺陷:

内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
并发容器之CopyOnWriteArrayList

8、Vector是否是线程安全的

Vector一定是线程安全的吗

参考文献
ArrayList源码分析

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值