Java学习笔记 -- ArrayList源码分析

1.ArrayList简介

ArrayList底层是用数组实现的,并且它是动态数组,也就是它的容量是可以自动增长的。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
  // 略...  
}
  • 实现RandomAccess接口:所以ArrayList支持快速随机访问,本质上是通过下标序号随机访问。

  • 实现Serializable接口:使ArrayList支持序列化,通过序列化传输。

  • 实现Cloneable接口:使ArrayList能够克隆。

1.1.底层关键

ArrayList底层本质上是一个数组,用该数组来保存数据:

transient Object[] elementData;

transient:Java关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

1.2.追踪源码

1.2.1.重要属性

// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 返回值Object类型的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储我们添加的数据,关键
transient Object[] elementData;
// 集合数据长度,因为没有给默认值所以就是0
private int size;

1.2.2.无参构造

1、在main函数实例化集合对象,并添加元素

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    // 1.首先添加10个元素
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
    // 2.再次添加5个元素
    for (int i = 10; i <= 15; i++) {
        list.add(i);
    }
    list.add(100);
    list.add(200);
}

2、在第一行设断点,进行Debug,进入到该类的无参构造函数:

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

可以看到,在构造函数里给存放数据的数组初始化容量大小为空,因为DEFAULTCAPACITY_EMPTY_ELEMENTDATA就是空数组。

所以这个时候 list = [ ]

4、第一次走到list.add(i)进入到源码里,如下:

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

可以看到,add()内部首先调用ensureCapacityInternal()方法,将集合数据长度先加一,再传入,也就是说第一次传入的是值是1,因为原先默认为0。该方法主要确定数组容量是否足够,是否需要扩容,并不涉及元素的添加,非常重要。

5、进入到该方法当中:

// minCapacity最小容量,首次为 size + 1 = 1
private void ensureCapacityInternal(int minCapacity) { 
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

可以看到,又调用了方法的重载,并且首先调用calculateCapacity()方法,并且将存放数据的数组和添加数据到当前时刻的大小传入,比如第一次添加数据,minCapatiry的大小等于1,第二次就等于2,然后调用重载方法,将calculateCapacity()计算的返回结果传入。

6、首先进入到calculateCapacity()

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
  • 第一次进入到这里边,elementData是空的,minCapacity = 1,而判断语句就是判断数组是否为空,所以第一次进入到该方法时,这里的判断条件是成立的。

  • 从默认容量DEFAULT_CAPACITY和当前数据大小minCapacity当中取一个最大值返回。因为第一次进入,DEFAULT_CAPACITY = 10minCapacity是传入的size + 1 = 1

还要一点需要注意的是:如果这里的minCapacity只在集合添加时使用,那直接返回DEFAULT_CAPACITY即可,因为首次添加数据时minCapacity为1,而DEFAULT_CAPACITY为10,一定大于minCapacity,而下次添加数据数组就不为空所以往后该判断均不成立,所以minCapacity不可能会比DEFAULT_CAPACITY 大,之所以还要调用方法挑选最大的返回,是因为ArrayList支持序列化和反序列化,如果直接通过反序列化获得一个集合扔过来,那么minCapacity的值可能是非常大的,这个时候是需要基于当前大小进行再次扩容。

所以、第一次最终的返回结果就是 10,这个10代表我们需要的数组容量大小,也就是说、首次添加数据发现数组为空,就直接扩容到10的大小。

6、进入重载方法:

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
  • 首先是modCount++,这是记录集合被修改的次数,因为ArrayList是非线程安全的。

  • 接着判断我们需要的最小容量减去当前数据长度是否大于0,如果条件成立,说明实际需要的容量大小已经大于原始数组容量的大小了,所以就调用grow()进行扩容,第一次添加数据,calculateCapacity()返回的结果是return Math.max(DEFAULT_CAPACITY, minCapacity),也就是10,说明我们实际需要的容量为10才够用,但是数组却是空的,所以条件必定成立,然后进行扩容,如下:

7、进入到grow()方法当中:

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

如下分析:

  1. 首先获取数组长度,赋值给oldCapacity,也就是0,这是原始容量大小。
  2. 然后进行位运算,向右移一位,也就是原始数据除以2,然后加上oldCapacity,由于是第一次,所以0 + 0 = 0,这是进行位运算后或者是扩容后的新容量大小。
  3. 这一步非常关键,判断扩容后的容量减去最小容量是否小于0,如果条件成立,就将执行newCapacity = minCapacity。也就是执行了newCapacity = 10,最后将新的容量复制给原始数组,也就是将一个空数组扩容到了大小为 10 的数组。

所以得出结论:初始化ArrayList数组默认为空,添加第一个数据之前将数组大小扩容到10,往后如果超出10,就会基于10进行1.5倍扩容,这里设计的非常绕,绕来绕去的就是为了扩容到10。

1.2.3.有参构造

如果使用的是有参构造,则初始化容量为指定大小,如下:

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

添加数据调用add()方法,进入calculateCapacity()方法,如下:

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

由于实例化的是有参构造,在里边指定了数组大小,这时判断就不成立了,因为数组不为空,而第一次添加数据minCapacity依旧为1。

接着进入ensureExplicitCapacity()

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

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

下方的条件依旧不成立,第一次添加数据minCapacity等于1,而数组大小可能为我们指定的8,所以不需要扩容。

所以:如果使用有参构造实例化集合对象,则初始化elementData为指定大小,如果需要扩容,则直接扩容elementData的1.5倍。

posted @ 2021-11-09 11:41  初晨~  阅读(48)  评论(0编辑  收藏  举报