List, 在java里是一个接口,其常见的实现类有ArrayListlinkedList, 但是一般在编程中用的最多的还是ArrayList,究其原因,ArrayList的底层是数组LinkedList的底层是双向链表, 而我们的开发需求中遍历的需求比增删要多,即使是增删也一般是在List的尾部添加元素即可,像是尾部添加元素,ArrayList的时间负责度也不过是O(1)。

对于ArrayList我们可以关注的第一个点就是:

java本身就存在数组了, 为什么要采用ArrayList呢?

  首先不说ArrayList封装了多个遍历的方法便利了我们平日里的开发, 在使用原生数组的时候会有一个特点:在你创建它的时候必须为它规定数组的大小,而ArrayList不用。

// 首先我们观察源码就会发现ArrayList实现数据存储的数据结构就是数组
transient Object[] elementData;

// 其次ArrayList在创建的时候不需要规定底层数组大小的原因是它的无参构造函数
public ArrayList() {
   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是个默认大小的空实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

需要注意的是当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量会扩容到默认大小10,至于这个第一次添加才真正扩充的操作也很简单,现在就带你看一看:

// 当你第一次添加元素时
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  //这一步 点进去
    elementData[size++] = e;	// 扩容到10后追加到尾部
    return true;
}

// 在ensureCapacityInternal()方法内部我们会看到该方法
/** @param elementData 存储数据的数据结构
*	@param minCapacity 当我们第一插入元素时, size为0 传入时是1
**/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
	// if判断当前数组是否是默认的空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    	//  获取默认的容量和传入参数的较大值
    	// 此时不用想都知道返回DEFAULT_CAPACITY(默认大小10)
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

其次,由于ArrayList底层实现也是数组,但它却不是规定死的,而是可以动态扩容的

ensureExplicitCapacity(int minCapacity)方法
  如果你自己跟着add()方法的源码一步一步走进去, 肯定会遇到这个方法:

// 判断是否需要扩容
 private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
    	// 调用grow方法进行扩容, 调用此方法表示已经开始扩容了
        grow(minCapacity);
}

我们来分析一下:

  • 当我们要add进第一个元素到ArrayList时,elementData.length还是0,又因为执行了ensureCapacityInternal()方法,所以此时minCapacity - elementData.length > 0 成立,所以会进入grow(minCapacity)方法
  • 当add第二个元素的时候,minCapacity为2, 此时elementData.length = 10 ,条件不成立,无法执行grow(minCapacity)方法
  • 添加3 、4…一直到第十个元素都不会执行grow(minCapacity)方法,数组的容量都为10

直到添加第11个元素, minCapacity大于elementData.length的时候才会进行扩容。

group()方法

private void grow(int minCapacity) {
    // 保存旧容量大小
    int oldCapacity = elementData.length;
    // 将oldCapacity右移一位,相当于/2, 写成这样是因为位运算更快
    // 整句的意思在扩容到原大小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);
}

// 另外hugeCapacity()方法 会比较minCapacity和MAX_ARRAY_SIZE。
//如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。

也就是说:

  • 当add第一个元素时, oldCapacity为0,此时第一个比较会成立,newCapacity = minCapacity=10。但第二个比较不会成立(private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;),所以此时数组容量为10
  • 当add第11个元素进入grow方法时,此时的newCapacity长度为15,第一个第二个比较都不成立,那么数组就会扩容到15,并以此类推。。

疑难杂惑:

我们在阅读ArrayList源码的时候会大量阅读到这两个函数, 比如一个出现在新增元素到指定下标的add()方法里

/**
*@param: elementData:源数组;
*@param: index:源数组中的起始位置;
*@param: elementData:目标数组;
*@param: index + 1:目标数组中的起始位置; 
*@param: size - index:要复制的数组元素的数量;
**/
System.arraycopy(elementData, index, elementData, index + 1, size - index);

还有个就出现在上面的grow()方法内:

// arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组
elementData = Arrays.copyOf(elementData, newCapacity);

好的到了这里就剩下一个困惑了, 为什么保存数据的elementData要被transient修饰呢?

大家都知道被transient修饰后的变量不会被序列化,但是要知道ArrayList是实现了Serializable这个标记接口的,难道我们在序列化一个ArrayList之后会读不到它的数据嘛? 肯定是不会这么荒谬的。 要知道,ArrayList通常都会预留一些存储容量,就比如说你new ArrayList<~>(); 之后add()操作四次后, 我们都知道它底层elementData.length此时长度为10, 所以就会存在一些空间是没有存储空间的,那么你直接序列化它不就浪费了额外的空间和时间了么?,我们来看源码

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // 将Size写到ObjectOutputStream
        s.writeInt(size);

        // 因为ArrayList是可扩容的,所以可能会存在一些没被利用的空间,所以这里只存有值的空间
        for (int i=0; i<size; i++) {	//size是实际存储对象个数
            s.writeObject(elementData[i]);	//有序的将element中已使用的对象读出到流中
        }

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

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) {
            // 就像clone()一样,根据大小而不是容量来分配数组
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // 正确的顺序读入所有对象.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
}

ArrayList讲完了, 可能还要看一眼Vector

Vector的底层数据结构也是数组(Object[] elementData;),但现在基本上看不到这位了。 相较于ArrayList,它是线程安全的,扩容的时候它会直接扩容两倍。 在Vector中,方法基本都被synchronized修饰,导致它的速度非常慢。

那还存在其他的线程安全的List么?

有,在java.util.concurrent包下还有一个类,叫做CopyOnWriteArrayList, 整个类的行为我觉得和文件系统非常相似, 文件系统在修改数据的时候,不会直接在原来的数据上进行操作,而是重新找个位置修改。比如:要修改数据块A的内容,先把A读出来,写到B里面去。这时候如果电脑挂了,原来A的内容还在,并且只有在真正发生修改的时候,才去分配资源,简单来说就是懒加载

再回来看看CopyOnWriteArrayList,它是一个线程安全的List,底层是通过复制数组来实现的。
直接看到它的add方法:

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

在add()方法他会加上lock锁,然后会复制出一个新的数组,往新的数组里add真正的元素,最后把array的指向改变为新的数组。并且如果你去观察get()方法或者size()方法只是获取array所指向的数组的元素或者大小。读不加锁, 写锁
但是这样大家应该明白它缺点是很耗费内容的,那次新增都复制一个数组。并且它还不能保证数据的一致性,假设A线程去读取数据,还没读完,现在B线程去清空了List,线程A照样能把剩余的数据读出来。