Java集合之ArrayList

基本特性

对于集合具体实现类来说,首先需要掌握的基本特性是:

  • 元素是否允许为null
  • 元素是否允许重复
  • 是否有序,指读取数据的顺序是否与存储数据的顺序一致
  • 是否线程安全

对于ArrayList,如下表:

基本特性 结论
元素是否允许为null
元素是否允许重复
是否有序
是否线程安全

源码分析

本文使用的是JDK 1.8.0_201的源码。

成员变量

ArrayList是一个底层以数组实现的集合,它最主要的成员变量是:

成员变量 作用
transient Object[] elementData; elementData作为底层数组
private int size; 集合中元素的个数,不同与elementData数组的长度

添加元素操作

ArrayList是用数组实现的,在Java中数组的长度是不可变的,数组在初始化的时候就要指定大小,我们知道ArrayList初始化时可以不指定大小,那么ArrayList是如何实现动态数组扩容的呢?

先看 add(E) 方法:

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

ArrayList每次添加元素时,都首先调用ensureCapacityInternal(size + 1)方法,确保数组的容量。我们跟到ensureCapacityInternal(size + 1)方法中去:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    // 这个值用于遍历时的快速失败,避免并发时导致的不可预料的错误
    modCount++;

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

上面calculateCapacity(elementData, minCapacity)方法判断ArrayList初始化时是否指定了大小,如果没有指定大小返回默认大小10。接着,modCount++操作与扩容关系不大,它用于遍历时的快速失败,避免并发时导致的不可预料的错误。最后的条件判断才是真正进行数组扩容的地方,继续跟到grow(minCapacity)方法中去:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 将数组的大小扩大的原来的1.5倍
    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);
}

int newCapacity = oldCapacity + (oldCapacity >> 1),这行代码将数组的大小扩大的原来的1.5倍。而进行数组扩容的代码Arrays.copyOf(elementData, newCapacity),底层调用的是System.arraycopy()方法,这个方法是本地方法,效率比较高。我们自己在实现数组扩容时,也可以直接调用这个本地方法,提高程序性能。

删除元素操作

ArrayList支持两种删除方式:

  • 按照下标进行删除
  • 按照元素进行删除,ArrayList允许元素重复,这个操作只会删除第一个匹配的元素

对于ArrayList来说,由于其底层是数组实现,那么数组在删除元素后,需要将该元素之后的每个元素都向前移动一个位置,因此效率是比较低的。

两种删除方式的逻辑略有不同,但是底层的删除操作都是下面这段代码:

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

插入元素操作

ArrayList插入元素的操作也是使用的add()方法,只不过参数不同:

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

同样地,由于ArrayList是数组实现,因此插入元素将导致插入位置后的所有元素向后移动一位,因此效率不高。

ArrayList的优缺点

优点:

  • 由于底层用数组实现,因此通过下标进行的随机访问,比如get(int index)、set(int index, E e)等操作会比较快。
  • 由于底层用数组实现,每个元素存储占用的内存空间相对较小。

缺点:

  • 由于底层用数组实现,在删除和插入元素时,需要移动元素,在元素较多情况下,效率会比较低。

综上所述,ArrayList只适合在对数据进行存储和访问的情况下使用,不适用频繁修改数据的场景。

ArrayList和Vector的区别

两者之间最大的区别就在于,ArrayList非线程安全,而Vector是线程安全的。尽管Vector是线程安全的,不代表在多线程的情况下就应该使用Vector。事实上我们应该避免使用Vector,因为Vector是在每个独立的方法上进行同步,而不是对整个集合数据进行同步,在进行迭代的时候可能会抛出ConcurrentModificationException。

除此之外,Vector即是“数组动态扩容”的实现又是同步操作的实现,违反了面向对象的“单一职责”设计原则。我们更应该使用装饰器模式的Collections.synchronizedList()。

无论是Vector还是Collections.synchronizedList(),采用的都是同步的方式来实现线程的安全性。这种方式将会降低并发性,当线程竞争激烈时,会严重影响程序的性能。Java 5.0提供了多种并发容器类,其中CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步List。

为什么ArrayList的elementData是用transient修饰的?

ArrayList中的数组,是这么定义的:

private transient Object[] elementData;

ArrayList是可以序列化的,而elementData被transient关键字修饰后,将不会被序列化,那么为什么要这么做呢?

因为ArrayList序列化时,elementData数组不一定恰好就是满的,比如elementData数组大小为10,而真正只存储了3个元素,那么为了提高序列化速度和减少序列化文件大小,程序只需要序列化有数据的3个元素,而不是整个elementData数组。为此,ArrayList重写了writeObject()方法:

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

参考

Java并发编程实战 P66-70
Stack Overflow :why-is-java-vector-and-stack-class-considered-obsolete-or-deprecated
五月的仓颉 :图解集合1:ArrayList

posted @ 2019-04-08 23:37  bluemilk  阅读(330)  评论(0编辑  收藏  举报