Java集合(1):ArrayList深度解析

一、ArrayList 的概述与特点

ArrayList就是动态数组,是数组Array的复杂版本,它具有以下特点:
(1)是一个动态数组,支持动态扩容
(2)有序存储,存储的元素可重复,并支持null元素的存储
(3)底层为数组,查找快,增删慢
(4)不支持同步,线程不安全



二、ArrayList 的继承体系

查看源码,发现ArrayList继承AbstractList抽象父类,实现了List、RandomAccess、Cloneable、Serializable接口。
其中:
(1)List接口:定义了List的一些操作规范。至于为什么继承了AbstractList还要再实现List接口的问题,网上有两种说法:一种是代码规范,方便直接看出是个List集合;二是为了是实现代理时,获得的接口中有List接口。
(2)RandomAccess接口:是一个空接口,代表可随机访问。实现了此接口的集合使用for循环遍历速度更快,而没有实现此接口的集合使用iterator迭代器遍历速度更快。
(3)Cloneable接口:空接口,实现此接口是为了可以调用clone()方法,ArrayList的clone()方法属于浅克隆。
(4)Serializable接口:空接口,代表可序列化
ArrayList继承体系





三、重要属性

(1)transient Object[] elementData;这是ArrayList的底层,是一个object数组。由transient修饰,代表此数组不参与序列化,而是使用另外的序列化方式(使用writeObject方法进行序列化,只序列化有值的位置,其他未赋值的位置不进行序列化,节省空间)。
(2)private int size;指集合包含的元素数量,注意与elementData.length(集合容量)的区别。比如当前集合容量为10,只存储了6个元素,则size为6,elementData.length为10。

四、构造方法

查看ArrayList源码所知,ArrayList有三个构造方法,一种为创建指定容量的集合,一种为无参的构造方法,最后一种为构造一个包含指定集合的构造方法,代码都比较简单,重点看一下无参的构造方法:

	/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
   
   
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的集合。注意,虽然在JDK1.8的注释中所说无参构造器创建的是一个初始容量为10的空集合。但是查看代码可知,刚开始创建的时候只是一个空集合,而容量变为10体现在后续的操作中(下文有介绍)。

五、增删改查

(1)add 方法

add方法有两个,分别public boolean add(E e)public void add(int index, E element),第一个是在集合元素的最后添加一个元素,第二个方法为在指定位置添加元素。代码如下:

public boolean add(E e) {
   
   
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
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++;
    }

由以上代码,我们可以发现两种add方法都调用了ensureCapacityInternal(size + 1)方法,此方法目的是为了确定是否扩容,第一个add方法比较简单,先判读是否扩容,然后再让最后一个元素的下一个等于需要添加的元素,重点看一下第二个add方法:

a.首先检查索引是否越界,如果越界则抛异常。调用的是rangeCheckForAdd(index);

    private void rangeCheckForAdd(int index) {
   
   
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

b.然后检测是否需要扩容,注意,扩容原理是比较重要的,调用的是ensureCapacityInternal(size + 1);注意,这里传入的参数minCapacity为数组存储的元素+1。

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

这里首先调用的是calculateCapacity(elementData, minCapacity),用来计算容量:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
   
   
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
   
   
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

代码的流程是如果现在的集合是空的,那么让它的容量等于DEFAULT_CAPACITY(值为10),否则返回minCapacity(值为size+1)。

然后调用的ensureExplicitCapacity(int minCapacity)方法,代码如下:

    private void ensureExplicitCapacity(int minCapacity) {
   
   
        modCount++;

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

其中modCount代表修改次数,是用在迭代器里保证遍历过程中集合不被其他线程修改。后面的代码很好理解,如果size+1(已经存储的元素+要存储的元素)大于集合容量,则调用扩容方法grow(minCapacity)

扩容方法grow(minCapacity)代码:

    private void grow(int minCapacity) {
   
   
        // overflow-conscious code
        int oldCapacity = elementData.length;
        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);
    }

旧的容量等于elementData.length,新的容量等于oldCapacity + (oldCapacity >> 1),这个地方新的容量等于旧的容量的1.5倍。从这里可以看出,动态扩容每次扩容1.5倍。

如果新容量小于size+1,返回size+1。这里适用的情况是第一次添加时elementData.length=0,那么旧容量和计算出的新容量都为0,而minCapacity 为之前得出的10,所以把10赋值给新容量。

然后则是对新容量如果超过数组最大长度的处理,最后调用了Arrays.copyOf(elementData, newCapacity)进行数组拷贝,跟踪代码,发现数组拷贝最终调用的是System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)方法,是一个native方法。

讲完了动态扩容的ensureCapacityInternal(size + 1)代码,后面的比较简单了,就是数组的复制,要添加元素的赋值,还有让size加一。

(2)remove 方法
由于删除不需要涉及扩容,所以代码比较简单,自行阅读即可。ArrayList提供了三种删除元素的方法,分别为remove(int index)remove(Object o)fastRemove(int index),即删除指定位置的元素、删除第一次出现的指定元素、跳过边界检查并且不返回删除的值的快速删除方法。
(2)setget方法
改查方法只需要检查是否索引越界,然后进行改查操作即可,代码也非常简单,不再赘述。


六、其他重要方法

(1)ensureCapacity(int minCapacity)
看这个方法的名字就是一个扩容的方法。我们从上文可知,每次进行扩容时都要进行一次数组复制,所以频繁的扩容会导致性能的浪费,所以最好的办法是在创建数组后就用此方法设置一个容量以减少扩容的次数。

public void ensureCapacity(int minCapacity) {
   
   
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
   
   
            ensureExplicitCapacity(minCapacity);
        }
    }

(2)trimToSize()
数组在复制完成后,它的size极有可能是小于它的容量的,为了减少空间的浪费,可以调用此方法来清空后面没有赋值的容量。

public void trimToSize() {
   
   
        modCount++;
        if (size < elementData.length) {
   
   
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

(3)clear()
如果一个ArrayList不再使用,可以调用clear()方法,让每个元素都为null,方便JVM回收。

public void clear() {
   
   
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

(4)线程安全问题
由于ArrayList在多线程环境下非安全的,多个线程同时操作时会产生并发问题,解决办法:
a.使用Collections工具类的方法:

 List list = Collections.synchronizedList(new ArrayList(...)); 

b.使用线程安全类Vector

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章