Java集合源码分析之List(二):ArrayList_一点课堂(多岸学院)

作了这么多准备,终于到了ArrayList了,ArrayList是咱们使用最为频繁的集合类了,咱们先看看文档是如何介绍它的:java

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)面试

可见,ArrayListVector的翻版,只是去除了线程安全。Vector由于种种缘由不推荐使用了,这里咱们就不对其进行分析了。ArrayList是一个能够动态调整大小的List实现,其数据的顺序与插入顺序始终一致,其他特性与List中定义的一致。数组

ArrayList继承结构

file

能够看到,ArrayListAbstractList的子类,同时实现了List接口。除此以外,它还实现了三个标识型接口,这几个接口都没有任何方法,仅做为标识表示实现类具有某项功能。RandomAccess表示实现类支持快速随机访问,Cloneable表示实现类支持克隆,具体表现为重写了clone方法,java.io.Serializable则表示支持序列化,若是须要对此过程自定义,能够重写writeObjectreadObject方法。安全

通常面试问到与ArrayList相关的问题时,可能会问ArrayList的初始大小是多少?不少人在初始化ArrayList时,可能都是直接调用无参构造函数,从未关注过此问题。例如,这样获取一个对象:dom

ArrayList<String> strings = new ArrayList<>();

咱们都知道,ArrayList是基于数组的,而数组是定长的。那ArrayList为什么不须要指定长度,就能使咱们既能够插入一条数据,也能够插入一万条数据?回想刚刚文档的第一句话:ide

Resizable-array implementation of the List interface.函数

ArrayList能够动态调整大小,因此咱们才能够无感知的插入多条数据,这也说明其必然有一个默认的大小。而要想扩充数组的大小,只能经过复制。这样一来,默认大小以及如何动态调整大小会对使用性能产生很是大的影响。咱们举个例子来讲明此情形:性能

好比默认大小为5,咱们向ArrayList中插入5条数据,并不会涉及到扩容。若是想插入100条数据,就须要将ArrayList大小调整到100再进行插入,这就涉及一次数组的复制。若是此时,还想再插入50条数据呢?那就得把大小再调整到150,把原有的100条数据复制过来,再插入新的50条数据。自此以后,咱们每向其中插入一条数据,都要涉及一次数据拷贝,且数据量越大,须要拷贝的数据越多,性能也会迅速降低。学习

其实,ArrayList仅仅是对数组操做的封装,里面采起了必定的措施来避免以上的问题,若是咱们不利用这些措施,就和直接使用数组没有太大的区别。那咱们就看看ArrayList用了哪些措施,而且如何使用它们吧。咱们先从初始化提及。优化

构造方法与初始化

ArrayList一共有三个构造方法,用到了两个成员变量。

//这是一个用来标记存储容量的数组,也是存放实际数据的数组。
//当ArrayList扩容时,其capacity就是这个数组应有的长度。
//默认时为空,添加进第一个元素后,就会直接扩展到DEFAULT_CAPACITY,也就是10
//这里和size区别在于,ArrayList扩容并非须要多少就扩展多少的
transient Object[] elementData;

//这里就是实际存储的数据个数了
private int size;

除了以上两个成员变量,咱们还须要掌握一个变量,它是

protected transient int modCount = 0;

这个变量主要做用是防止在进行一些操做时,改变了ArrayList的大小,那将使得结果不可预测。

下面咱们看看构造函数:

//默认构造方法。文档说明其默认大小为10,但正如elementData定义所言,
//只有插入一条数据后才会扩展为10,而实际上默认是空的
 public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//带初始大小的构造方法,一旦指定了大小,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);
    }
}

//从一个其余的Collection中构造一个具备初始化数据的ArrayList。
//这里能够看到size是表示存储数据的数量
//这也展现了Collection这种抽象的魅力,能够在不一样的结构间转换
public ArrayList(Collection<? extends E> c) {
    //转换最主要的是toArray(),这在Collection中就定义了
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

重要方法

ArrayList已是一个具体的实现类了,因此在List接口中定义的全部方法在此都作了实现。其中有些在AbstractList中实现过的方法,在这里再次被重写,咱们稍后就能够看到它们的区别。

先看一些简单的方法:

//还记得在AbstractList中的实现吗?那是基于Iterator完成的。
//在这里彻底不必先转成Iterator再进行操做
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

//和indexOf是相同的道理
 public int lastIndexOf(Object o) {
    //...
}

//同样的道理,已经有了全部元素,不须要再利用Iterator来获取元素了
//注意这里返回时把elementData截断为size大小
public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

//带类型的转换,看到这里a[size] = null;这个用处真不大,除非你肯定全部元素都不为空,
//才能够经过null来判断获取了多少有用数据。
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 给定的数据长度不够,复制出一个新的并返回
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

数据操做最重要的就是增删改查,改查都不涉及长度的变化,而增删就涉及到动态调整大小的问题,咱们先看看改和查是如何实现的:

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

//只要获取的数据位置在0-size之间便可
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

//改变下对应位置的值
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

增和删是ArrayList最重要的部分,这部分代码须要咱们细细研究,咱们看看它是如何处理咱们例子中的问题的:

//在最后添加一个元素
public boolean add(E e) {
    //先确保elementData数组的长度足够
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index);

    //先确保elementData数组的长度足够
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //将数据向后移动一位,空出位置以后再插入
    System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
    elementData[index] = element;
    size++;
}

以上两种添加数据的方式都调用到了ensureCapacityInternal这个方法,咱们看看它是如何完成工做的:

//在定义elementData时就提过,插入第一个数据就直接将其扩充至10
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    
    //这里把工做又交了出去
    ensureExplicitCapacity(minCapacity);
}

//若是elementData的长度不能知足需求,就须要扩充了
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

//扩充
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //能够看到这里是1.5倍扩充的
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    //扩充完以后,仍是没知足,这时候就直接扩充到minCapacity
    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);
}

至此,咱们完全明白了ArrayList的扩容机制了。首先建立一个空数组elementData,第一次插入数据时直接扩充至10,而后若是elementData的长度不足,就扩充1.5倍,若是扩充完还不够,就使用须要的长度做为elementData的长度。

这样的方式显然比咱们例子中好一些,可是在遇到大量数据时仍是会频繁的拷贝数据。那么如何缓解这种问题呢,ArrayList为咱们提供了两种可行的方案:

  • 使用ArrayList(int initialCapacity)这个有参构造,在建立时就声明一个较大的大小,这样解决了频繁拷贝问题,可是须要咱们提早预知数据的数量级,也会一直占有较大的内存。
  • 除了添加数据时能够自动扩容外,咱们还能够在插入前先进行一次扩容。只要提早预知数据的数量级,就能够在须要时直接一次扩充到位,与ArrayList(int initialCapacity)相比的好处在于没必要一直占有较大内存,同时数据拷贝的次数也大大减小了。这个方法就是ensureCapacity(int minCapacity),其内部就是调用了ensureCapacityInternal(int minCapacity)

其余还有一些比较重要的函数,其实现的原理也大同小异,这里咱们不一一分析了,但仍是把它们列举出来,以便使用。

//将elementData的大小设置为和size同样大,释放全部无用内存
public void trimToSize() {
    //...
}

//删除指定位置的元素
public E remove(int index) {
    //...
}

//根据元素自己删除
public boolean remove(Object o) {
    //...
}

//在末尾添加一些元素
public boolean addAll(Collection<? extends E> c) {
    //...
}

//从指定位置起,添加一些元素
public boolean addAll(int index, Collection<? extends E> c){
    //...
}

//删除指定范围内的元素
protected void removeRange(int fromIndex, int toIndex){
    //...
}

//删除全部包含在c中的元素
public boolean removeAll(Collection<?> c) {
    //...
}

//仅保留全部包含在c中的元素
public boolean retainAll(Collection<?> c) {
    //...
}

ArrayList还对父级实现的ListIterator以及SubList进行了优化,主要是使用位置访问元素,咱们就再也不研究了。

其余实现方法

ArrayList不只实现了List中定义的全部功能,还实现了equalshashCodeclonewriteObjectreadObject等方法。这些方法都须要与存储的数据配合,不然结果将是错误的或者克隆获得的数据只是浅拷贝,或者数据自己不支持序列化等,这些咱们定义数据时注意到便可。咱们主要看下其在序列化时自定义了哪些东西。

//这里就能解开咱们的迷惑了,elementData被transient修饰,也就是不会参与序列化
//这里咱们看到数据是一个个写入的,而且将size也写入了进去
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]);
    }

    //modCount的做用在此体现,若是序列化时进行了修改操做,就会抛出异常
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

readObject是一个相反的过程,就是把数据正确的恢复回来,并将elementData设置好便可,感兴趣能够自行阅读源码。

总结

整体而言,ArrayList仍是和数组同样,更适合于数据随机访问,而不太适合于大量的插入与删除,若是必定要进行插入操做,要使用如下三种方式:

  • 使用ArrayList(int initialCapacity)这个有参构造,在建立时就声明一个较大的大小。
  • 使用ensureCapacity(int minCapacity),在插入前先扩容。
  • 使用LinkedList,这个无可厚非哈,咱们很快就会介绍这个适合于增删的集合类。

【感谢您能看完,若是可以帮到您,麻烦点个赞~】

更多经验技术欢迎前来共同窗习交流: 一点课堂-为梦想而奋斗的在线学习平台 http://www.yidiankt.com/

![关注公众号,回复“1”免费领取-【java核心知识点】] file

QQ讨论群:616683098

QQ:3184402434

想要深刻学习的同窗们能够加我QQ一块儿学习讨论~还有全套资源分享,经验探讨,等你哦! 在这里插入图片描述

相关文章
相关标签/搜索