笔记--ArrayList初始化&扩容

一、序
    ArrayList作为常用的集合,频繁的出现在工作和面试中,今天咱们从源码层面来复习一下有关ArrayList的一些知识。
 
二、
1.简介
  ArrayList底层是数组队列,可以动态的扩容,
它实现了java.io.Serializable接口,支持序列化,
它实现了Cloneable接口,可以被克隆,
它实现了 RandomAccess 接口,支持快速随机访问(根据下标获取元素)。
ArrayList可以根据数组下标快速的读取元素,他的查询时间复杂度为O(1),因此ArrayList适用于查询频繁的场景,反之它的插入删除操作的时间复杂度为O(n)。
 
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{...}
 
2.ArrayList是否线程安全?
    ArrayList是线程不安全的,在多线程操作同一个数组时,可能会导致数组下标越界,举例:
List<String> list = new ArrayList<String>();
假设已经添加了9个元素,此时list的size = 9,
private static final int DEFAULT_CAPACITY = 10;//默认大小
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 
public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    //根据下标赋值
    elementData[size++] = e;
    return true;
}
 
private void ensureCapacityInternal(int minCapacity) {
    //判断数组是否为空,如果为空最小扩容量为默认大小10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //判断是否需要扩容
    ensureExplicitCapacity(minCapacity);
}
 
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    //最小扩容量大于当前数组长度就需要扩容
    if (minCapacity - elementData.length > 0)
        //开始扩容方法
        grow(minCapacity);
}
 
thread_1 调用 list.add("a");走到ensureCapacityInternal(size + 1)方法时挂起,此时并不需要扩容;
thread_2 调用 list.add("b");发现数组不需要扩容,直接将"b"放在下标为9的位置,此时size=10;
然后thread_1 继续执行,尝试将"a"放在下标为10的位置,但是数组并没有扩容,最大下标为9,所以程序抛出数组下标越界异常。并且,elementData[size++] = e也不是原子操作,多线程操作时可能会导致值覆盖的问题。
所以当多线程的情况下可以考虑使用Vector或者CopyOnWriteArrayList
 
3.ArrayList扩容机制
ArrayList的无参构造是创建一个空的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这种方式创建一个ArrayList时,在第一次调用add()方法的时候就会进行一次扩容操作,在上面的ensureCapacityInternal()和ensureExplicitCapacity()方法中我们可以看到
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//最大容量
//扩容方法
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;//旧容量
    //位运算,新容量 = 旧容量 + 旧容量/2
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //判断新容量是否小于最小所需容量,true -> 新容量 = 最小所需容量
    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) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //如果新容量超出最大容量,则新容量为Interger.MAX_VALUE,否则为MAX_ARRAY_SIZE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
所以在使用ArrayList时,最好可以给定一个初始值大小,这样减少扩容的次数,提升性能
//指定初始大小的有参构造(还有一种是直接传入一个Collection集合)
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);
    }
}
 
PS:再一个值的注意的就是在ArrayList源码中频繁的用到了Arrays.copyOf()和System.arraycopy()两个方法,简单说下两种方式的区别:
    arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置
    copyOf()是系统自动在内部新建一个数组,并返回该数组。
其实在Arrays.copyOf()内部调用了System.arraycopy()方法。
4.小结
总体来说ArrayList的源码还是比较容易看明白的,除去扩容方法其他的方法也是值的学习的,以上内容如果有不正确的地方欢迎大家指正批评。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章