Java基础知识之容器(二:ArrayList详解)

java框架系列文章相关地址:

Java基础知识之容器(一:容器整体框架探索)
Java基础知识之容器(二:ArrayList详解)
Java基础知识之容器(三:LinkedList详解)
Java基础知识之容器(四:Vector详解)
Java基础知识之容器(五:HashSet详解)
Java基础知识之容器(六:TreeSet详解)
Java基础知识之容器(七:HashMap详解)
Java基础知识之容器(八:HashMap在jdk8数据结构的改进)

我们先来看看ArrayList的实现关系:
ArrayList继承于AbstractList,AbstractList实现了List接口,List接口继承于Collection接口。Collection是集合层次的根接口,List有三个常用的实现类,分别是ArrayList、LinkedList、Vector。这节我们重点介绍ArrayList。

如果对容器整体框架还不清楚的请移步这里:Java基础知识之容器(一:容器整体框架探索)

ArrayList内部是使用数组实现的,换句话说,ArrayList封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素或者数据的扩展和重定向。

数组是在内存中划分出一块连续的地址空间来进行元素的存储,由于它直接操作内存,所以数组是一种效率最高的存储和随机访问对象引用序列的方式。但是数组也存在致命的缺陷,就是在初始化的时候必须指定大小,大小一旦确定不能再更改。在实际情况中我们遇到更多的是一开始并不知道要存放多少元素,而是希望容器能够自动的扩展它自身的容量以便能够存放更多的元素。

ArrayList就能够很好的满足这样的需求,它能够自动扩展大小以适应存储元素的不断增加。它的底层是基于数组实现的,因此它具有数组的一些特点,例如查找修改快而插入删除慢。

ArrayList特点:

  • 快速随机访问
  • 允许存放多个null对象
  • 底层是Object数组
  • 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),改对应索引元素比较快

先简单介绍一下System.arraycopy()这个函数,我以前用的不多,所以这里温故一下

/**
* @param      src      the source array.
* @param      srcPos   starting position in the source array.
* @param      dest     the destination array.
* @param      destPos  starting position in the destination data.
* @param      length   the number of array elements to be copied.
**/
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

下面举个栗子:

public class Test {

    public static void main(String[] args) {
        int[] a = {0, 1, 2, 3, 4, 5, 6};
        System.arraycopy(a, 3, a, 2, 4);
        System.out.print(Arrays.toString(a));
    }
}

输出:

[0, 1, 3, 4, 5, 6, 6]

由于这个函数是native函数,底层是由C来做数组的拷贝,效率会高一些,所以对数组拷贝这个方法理解了,下面会大量用到数组拷贝这个函数。

下面源码探究:

一:类信息

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作.对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类.
  • 实现了List接口,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合.
  • 实现了RandomAccess接口标识着其支持随机快速访问,实际上,我们查看RandomAccess源码可以看到,其实里面什么都没有定义.因为ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1).
  • 实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制
  • 实现了Serializable 标识着集合可被序列化。

二:全局变量

 /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * ArrayList的元素存储在其中的数组缓冲区。 ArrayList的容量是这个数组缓冲区的长度。 当添加第一个元素时,任何具有elementData == EMPTY_ELEMENTDATA的空ArrayList将展开为DEFAULT_CAPACITY。
     */
    transient Object[] elementData;

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

三:ArrayList的3个构造函数:

1、无参构造函数:

public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

创建了一个容量为0的空数组。

2、创建一个指定大小容量的数组的构造函数

public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

3、创建一个包含指定集合内容的数组

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();//转换成数组,赋值给Object数组
        size = elementData.length;//初始化数组大小
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)//如果目标数组的真实类型(存在Object子类型的情况)不等于Object类型,则创建一个Object数组并把元素拷贝进去。
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

这里有一点需要说明一下,c.toArray 可能不返回 Object[] ,这里其实也很好理解,java对象允许向上转型,也就是说子类数组转父类数组是允许的,比如: List< String> list = Arrays.asList(“abc”); Object[] a = list.toArray(); 所以当我们a中添加Object对象时,由于数组中的元素类型都是String类型,所以会报错java.lang.ArrayStoreException。这也就是说假如我们有1个Object[]数组,并不代表着我们可以将Object对象存进去,这取决于数组中元素实际的类型。

四、添加元素

1、向末尾添加一个元素

public boolean add(E object) {
        ensureCapacityInternal(size + 1);  //确保内部数组有足够的空间
        elementData[size++] = e;//将元素加到数组末尾
        return true;
    }


private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {//如果为空数组,所需容量为默认容量和所需容量取较大值,也就是说最少需要容量为DEFAULT_CAPACITY 10.
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

 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;
        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) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

可以看到,只要arrayList的当前容量足够大,add()操作的效率是非常高的,只有当ArrayList对容量的要求超出当前容量大小时,才需要进行扩容,扩容的过程中会进行大量的数组复制操作,而数组复制时,最终将调用System.arraycopy()方法,因此add()操作的效率还是相当高的。

这里有一点需要说一下,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;我们都知道整形的最大值为2^31 -1,MAX_ARRAY_SIZE 为啥要减去8,我们根据注释可以了解到, 一些VM在阵列中保留一些标题字,尝试分配较大的数组可能会导致OutOfMemoryError:请求的数组大小超出VM限制,但是当我们达到MAX_ARRAY_SIZE 的时候再去扩容时,还会给我们分配剩余的全部空间Integer.MAX_VALUE。

2、向指定位置添加一个元素

public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置后的所有元素需要重新排列,因此,其效率还是比较低的。

五、移除元素

public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) 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;
    }

把数组元素从index+1位置向前移动一位,然后把最后一位置空。

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
private void fastRemove(int index) {
        modCount++;
        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
    }

移除index元素其实就是把index后面的数据全部向前移动一位然后再把最后一个位置置null节省内存空间。
可以看到,在ArrayList的每一次有效的元素删除操作后,都要进行数组的重组,并且删除的位置越是靠前,重组的开销越大。

最后再来看一下clear

public void clear() {
        modCount++;

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

        size = 0;
    }

猜你喜欢

转载自blog.csdn.net/u013277209/article/details/80170116