ArrayList分析

115 阅读7分钟

ArrayList分析

基于Java版本1.8

一、概述

1、特点

  • ArrayList是一个可变长列表,当列表满时自动扩容为1.5倍。
  • 底层实现是一个对象数组。所以向指定位置加入元素或者删除元素,会导致数组元素进行移动,所以只适合在尾部添加或者覆盖元素。
  • 非线程安全,在并发情况下可能导致一些数据被覆盖,导致最终结果不可预期。
  • 支持RandomAccess-快速随机访问-既通过数组下标O(1)时间复杂度进行访问。

2、关键字段

//默认容量大小,用于在没有指定容量时或者容量为0时 第一次扩容后容量
private static final int DEFAULT_CAPACITY = 10;

/**
 * 一个共享的空元素数组,当初始化容量为0的时候使用,这样可以避免重复创建一个空数组
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 一个共享空的默认容量的元素数据,当add元素时发现elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA时 扩容到默认容量10
 * 用于延迟创建 elementData这一实际存放数据的数组,为区别每次扩容多少(例如批量add时)所以和上面单独区分
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 *存储元素的数组缓冲区。 ArrayList 的容量就是这个数组缓冲区的长度。
 *当添加第一个元素时,任何具有 elementData ==DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList都将扩展为 DEFAULT_CAPACITY。
 *这里用transient修饰表示不需要序列,那么基本上可以肯定它是自己实现了序列化方式的
 */
transient Object[] elementData; 

/**
 * 表示当前存了多少个元素
 */
private int size;

二、初始化

1、指定容量初始化

如果指定了容量且大于0则按指定容量初始化。需要注意的是:当指定容量为0时,并不是直接创建一个空elementData数组,而是用EMPTY_ELEMENTDATA这个代替,避免重复创建一个无意义空对象数组。

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

2、无参初始化

默认容量为10,但是这个默认容量不是指的在构建时就new 一个容量为10的数组,而是在add(添加单个)时任何elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA都会先扩容为10;这个在1.7

public ArrayList() {
    //并不是在构造时就创建一个10容量的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

3、传入集合

基于已有集合创建,这里做了一个小优化如果传入的是ArrayList可直接使用Collection.toArray返回的数组(其他实现该方法的类不一定返回的是新创建的数组),反之则需要通过Arrays.copyOf复制出一个和其一样的数组,这样可以避免新的ArrayList影响到传入的集合。

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

三、扩容机制

1、何时触发扩容

每次添加元素时都会检测容量。扩容出现在向ArrayList添加元素且ArrayList容量不够存下添加元素的时,扩容到需要容量的1.5倍。当然需要注意的是默认容量10时在创建时没有初始化,那么需要取10和需要的容量中的较大值作为最终需要容量。

public boolean add(E e) {
    //需要的最小容量=size + 1
     ensureCapacityInternal(size + 1);  // Increments modCount!!
     elementData[size++] = e;
     return true;
}
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    //需要的最小容量=size + numNew
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

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

//计算需要容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //当是默认容量未初始化时 需要取默认容量10和需要的容量的最大值。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

//当需要容量大于当前长度触发扩容
private void ensureExplicitCapacity(int minCapacity) {
     //fail-fast机制
     modCount++;
     // overflow-conscious code
     if (minCapacity - elementData.length > 0)
         //触发扩容
         grow(minCapacity);
}

2、扩容实现

可以看到新容量大小为旧容量的1.5倍。但是int数值在Java中只有32位,需要的最小容量都溢出了(体现为<0)则OutOfMemoryError无法在创建更大的数组了。

//扩容 minCapacity 最小容量
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //1.5倍溢出则取需要容量
        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);
}
//最大容量
private static int hugeCapacity(int minCapacity) {
    //需要容量溢出了则OOM
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
//jvm会占用一些空间 所以可能申请超过MAX_ARRAY_SIZE就出现OOM
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

四、fail-fast机制

JDK提供的一种机制,在使用迭代器进行遍历时检测List的结构是否改变的机制。当发现改变直接抛出异常-ConcurrentModificationException。这样可以遍历里快速感知到结构是否发生改变,提前发现迭代遍历因结构发生改变导致异常的问题。

1、实现原理

  1. modCount记录List结构修改的次数。在ArrayList只要结构发生改成都会使modCount++

    //记录修改次数
    protected transient int modCount = 0;
    //-----------------ArrayList中修改结构方法-------------
    //add时 modCount++;
    private void ensureExplicitCapacity(int minCapacity) {
       modCount++;
    
       // overflow-conscious code
       if (minCapacity - elementData.length > 0)
           grow(minCapacity);
    }
    //移除也记录了修改次数
     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;
    }
    
  2. 在迭代器创建时就记录当前的modCount作为expectedModCount

    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;
    }
    
  3. 在遍历时判断期望的修改次数是否相等

    //迭代器的取下一个方法
    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];
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            //不相等则抛出异常
            throw new ConcurrentModificationException();
    }
    

2、如何在迭代的时候删除

使用迭代器中中remove方法,该方法在删除元素后同步modCount,也就是不会出现expectedModCountmodCount不相等的问题。但是只是保证了单线程的迭代删除没有问题。

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        //修改为当前的modCount 
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

3、线程安全的方式

  • Collections.synchronizedList

    该方法将List封装为SynchronizedList,在这个类中使用**synchronized代码块**来保证操作List的中的元素是线程安全的。

    //装饰的的list
    final List<E> list
    //--------------------都是synchronized代码块-----------------------
    public E get(int index) {
        synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }
    

    虽然保证了增加、删除以及访问的线程安全,但是对使用迭代器没有做任何限制,所以在使用迭代器做遍历且在并发的情况下依旧会出现线程安全问题,所以

    用户在迭代返回的列表时必须手动同步它:

    List list = Collections.synchronizedList(new ArrayList());
                ...
    synchronized (list) {
        Iterator i = list.iterator(); // Must be in synchronized block
        while (i.hasNext())
            foo(i.next());
    }
    
  • CopyOnWriteArrayList

    CopyOnWriteArrayList通过ReentrantLock来保证线程安全,并且每次修改结构都是Copy出一个新的元素数组,这样迭代器生命周期内元素数组不会被修改进而保证的迭代器的线程安全性。

    //元素数组 只能通过getArray和setArray访问 volatile 保证可见性
    private transient volatile Object[] array;
    
    //添加元素是直接加加锁,然后copy出一个新的元素数组
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            //进行复制
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            //替换原来
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }
    //其他remove,addAll等可修改结构的方法都是差不多代码
    
    //创建迭代器是传了元素数组 
    public Iterator<E> iterator() {
       return new COWIterator<E>(getArray(), 0);
    }
    
    

    虽然保证迭代器不会出现ConcurrentModificationException,但是迭代器的修改无法映射到CopyOnWriteArrayList中。可以看其迭代器实现-COWSubListIterator对应这些操作是直接抛出UnsupportedOperationException

    //COWSubListIterator部分代码
    public void remove() {
        throw new UnsupportedOperationException();
    }
    
    public void set(E e) {
        throw new UnsupportedOperationException();
    }
    
    public void add(E e) {
        throw new UnsupportedOperationException();
    }
    

    还有一个问题是,每次修改结构都要进行一次Copy,通常成本比较高。适合于遍历远远高于修改且在不能或不想同步遍历但需要排除并发线程之间的干扰。

  • Vector

    Vector也是线程安全的List,它是通过synchronized方法实现的,它为所有可能出现线程安全的方法都以synchronized修饰。同样它和SynchronizedList在迭代时是需要自行编写同步代码。

    补充:Vector在扩容时和ArrayList不同,它可以配置每次扩容的大小,如果没有配置则翻倍,而ArrayList是扩容为1.5倍

    //增加容量
    protected int capacityIncrement;
        
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //扩展指定大小,或者翻倍
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }