ArrayList源码分析(JDK1.8)

简述ArrayList

  • ArrayList 是一个动态数组,它是线程不安全的,允许元素为null。 
  • 其底层数据结构依然是数组,它实现了List<E>, RandomAccess, Cloneable, java.io.Serializable接口,其中RandomAccess代表了其拥有随机快速访问的能力,ArrayList可以以O(1)的时间复杂度去根据下标访问元素。
  • 因其底层数据结构是数组,所以可想而知,它是占据一块连续的内存空间(容量就是数组的length),所以它也有数组的缺点,空间效率不高
  • 由于数组的内存连续,可以根据下标以O(1)的时间读写(改查)元素,因此时间效率很高
  • 当集合中的元素超出这个容量,便会进行扩容操作。扩容操作也是ArrayList 的一个性能消耗比较大的地方,所以若我们可以提前预知数据的规模,应该通过public ArrayList(int initialCapacity) {}构造方法,指定集合的大小,去构建ArrayList实例,以减少扩容次数,提高效率

ArrayList结构

看一下继承、实现了什么

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

我们首先需要明白并且牢记在内心的是,ArrayList本质上是一个数组,但是与Java中基础的数组所不同的是,它能够动态增长自己的容量

通过ArrayList的定义,可以知道ArrayList继承了AbstractList,同时实现了List,RandomAccess,Cloneable和java.io.Serializable接口。

那么这些提供了什么功能呢?

  • 继承了AbstractList类,实现了List,意味着ArrayList是一个数组队列,提供了诸如增删改查、遍历等功能。
  • 实现了RandomAccess接口,意味着ArrayList提供了随机访问的功能。RandomAccess接口在Java中是用来被List实现,用来提供快速访问功能的。在ArrayList中,即我们可以通过元素的序号快速获取元素对象。
  • 实现了Cloneable接口,意味着ArrayList实现了clone()函数,能被克隆。
  • 实现了java.io.Serializable接口,意味着ArrayList能够通过序列化进行传输或者持久保存。

属性分析

/**
 * 默认初始容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 共享的空数组
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 使用默认大小的共享的空数组
 * EMPTY_ELEMENTDATA的区别:当向数组添加第一个元素时,知道数组该扩容多少.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * ArrayList基于该数组实现,用该数组保存数据
 * ArrayList的容量是该数组的长度
 * 数组的默认大小为DEFAULT_CAPACITY
 * transient:在实现Serializable接口后,将不需要序列化的属性前面加上transient
 */
transient Object[] elementData; // 没有被私有化是为了简化内部类访问

/**
 * ArrayList的大小(实际所含元素的个数)
 */
private int size;

/**
 * 被修改的次数
 */
protected transient int modCount = 0;

/**
 * 数组的最大值
 * -8是因为要保留数组的一些头信息
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造器

/**
 * 给定容量的构造器
 */
public ArrayList(int initialCapacity) {
    //如果给的容量为正数,则数组初始化就是这个值
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
        //如果为0就采用默认
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
        //否则抛出异常
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    }
}

/**
 * 无参构造器,默认容量为10
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 带泛型参数的构造器
 */
public ArrayList(Collection<? extends E> c) {
    //先把参数转为数组类型
    //toArray()方法重载自
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 官方的bug,说不一定能返回Object[]类型
        if (elementData.getClass() != Object[].class)
            //如果真的不是Object[]类型,就强制转回Object[]类型
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果传入的容器参数为0,就把他替换为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

常用方法

增加

add(E e)

/**
 * 在数组末尾增加元素
 * 先不管ensureCapacityInternal的话,
 * 这个方法就是将一个元素增加到数组的size位置上,然后size=size+1 * 再说回ensureCapacityInternal,它是用来扩容的,准确说是用来进行扩容检查的。下面我们来看一下整个扩容的过程
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 下面的操作可分为下面两步
    // elementData[size] = e
    // size=size+1
    elementData[size++] = e;
    return true;
}

这个add(E e)函数涉及很多函数,下面逐一分析

// 检查容量大小
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算容量大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 从默认容量和穿进来的容量选择一个最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //直接返回minCapacity
    return minCapacity;
}

// 扩容判断
private void ensureExplicitCapacity(int minCapacity) {
    //记录修改的次数
    modCount++;

    // 判断是否需要扩容
    // elementData.length数组的大小,并不是数组的元素个数,size才是
    if (minCapacity - elementData.length > 0)
        // 进行真正的扩容操作
        grow(minCapacity);
}


扩容

重点:grow是真正的扩容操作,所以单独拿出来讲

/**
 * 进行真正的扩容操作
 */
private void grow(int minCapacity) {
    // 获取旧的列表大小
    int oldCapacity = elementData.length;
    // 新的容量是在原有的容量的基础上+50% 右移一位就是二分之一
    // 上面1>>表示右移,也就是相当于除以2,减为一半
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果扩容一半之后还不足,则新的容器大小等于minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新的容量大于MAX_ARRAY_SIZE,则进行hugeCapacity()操作
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 复制原先的数组,并给他一个新容量
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 最大不能超过Integer.MAX_VALUE
private static int hugeCapacity(int minCapacity) {
    // 因为一旦大小超过了Integer.MAX_VALUE,数值就会为负数
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}


扩容机制如下:

  1. 先默认将列表大小newCapacity增加原来一半,即如果原来是10,则新的大小为15;
  2. 如果新的大小newCapacity依旧不能满足add进来的元素总个数minCapacity,则将列表大小改为和minCapacity一样大;即如果扩大一半后newCapacity为15,但add进来的总元素个数minCapacity为20,则15明显不能存储20个元素,那么此时就将newCapacity大小扩大到20,刚刚好存储20个元素;
  3. 如果扩容后的列表大小大于2147483639,也就是说大于Integer.MAX_VALUE - 8,此时就要做额外处理了,因为实际总元素大小有可能比Integer.MAX_VALUE还要大,当实际总元素大小minCapacity的值大于Integer.MAX_VALUE,即大于2147483647时,此时minCapacity的值将变为负数,因为int是有符号的,当超过最大值时就变为负数

删除

/**
 * 删除指定位置的元素
 * 把这个元素后面的元素全部往左移一位(下标减一)
 */
public E remove(int index) {
    // 数组下标越界检查
    rangeCheck(index);

    // 记录修改次数
    modCount++;
    // 得到要删除的元素
    E oldValue = elementData(index);

    // 需要移动的元素的数量=实际元素个数-当前要删除元素下标-1
    int numMoved = size - index - 1;
    // 如果这个值大于0,说明后续还有元素需要左移
    if (numMoved > 0)
        // 被删除元素的下标为index
        // 删除原理:index之后的所有元素都往前移一位,覆盖前面的元素,总共需要移动numMoved个元素
        System.arraycopy(elementData, index+1, elementData, index,
                numMoved);
    // 最后一个元素的值赋值为null,这样就可以被GC回收了
    elementData[--size] = null;

    // 返回删除的值
    return oldValue;
}

常见问题:

ArrayList为什么线程不安全?

主要分析ArrayList中的add()函数为什么线程不安全

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

通过上面的分析可以看到add()函数中有两个步骤,这两个步骤都在多线程的情况下都有可能会出现问题

首先分析第一个步骤:ensureCapacityInternal(size+1);

我这里画了一个EXCEL方便分析


然后分析第二个步骤elementData[size++] = e;

其实这里就是size++;不是线程安全


ArrayList与LinkedList区别

  • List是接口类,ArrayList和LinkedList是List的实现类。
  • ArrayList是动态数组(顺序表)的数据结构。顺序表的存储地址是连续的,所以在查找比较快,但是在插入和删除时,由于需要把其它的元素顺序向后移动(或向前移动),所以比较耗时。
  • LinkedList是链表的数据结构。链表的存储地址是不连续的,每个存储地址通过指针指向,在查找时需要进行通过指针遍历元素,所以在查找时比较慢。由于链表插入时不需移动其它元素,所以在插入和删除时比较快。

ArrayList和LinkedList的时间复杂度

ArrayList 是线性表(数组)

  • get() 直接读取第几个下标,复杂度 O(1)
  • add(E) 添加元素,直接在后面添加,复杂度O(1)
  • add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)
  • remove() 删除元素,后面的元素需要逐个移动,复杂度O(n)
LinkedList 是链表的操作
  • get() 获取第几个元素,依次遍历,复杂度O(n)
  • add(E) 添加到末尾,复杂度O(1)
  • add(index, E) 添加第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n)
  • remove() 删除元素,直接指针指向操作,复杂度O(1)

ArrayList和Vector的区别

  • ArrayList是线程不安全的,Vector是线程安全的
  • 扩容时候ArrayList扩0.5倍,Vector扩1倍

ArrayList有没有办法线程安全?

Collections工具类有一个synchronizedList方法

可以把list变为线程安全的集合,但是意义不大,因为可以使用Vector

Vector为什么是线程安全的?

通过对比ArrayList和Vector的源码可以清楚的看到,Vector之所以线程安全是因为加了大量的synchronized



如何复制某个ArrayList到另一个Arraylist中去?

  1. 使用clone()方法,比如ArrayList newArray = oldArray.clone();
  2. 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
  3. 使用Collection的copy方法。

  注意1和2是浅拷贝(shallow copy)。

浅拷贝和深拷贝的定义

  • 浅拷贝:只复制一个对象,对象内部存在的指向其他对象数组或者引用则不复制
  • 深拷贝:对象,对象内部的引用均复制

为了更好的理解它们的区别我们假设有一个对象A,它包含有2对象对象A1和对象A2

对象A进行浅拷贝后,得到对象B但是对象A1和A2并没有被拷贝

对象A进行深拷贝,得到对象B的同时A1和A2连同它们的引用也被拷贝


参考:

https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/2-ArrayList.md

https://juejin.im/post/5b2c5eefe51d4558c0442e95?utm_source=gold_browser_extension

http://developer.51cto.com/art/200905/124592.htm

猜你喜欢

转载自blog.csdn.net/CrankZ/article/details/80969514