集合篇:List—ArrayList源码解析


ArrayList是常用的结构,也是面试过程中可能被问到的知识点,需要对源码的细节进行了解。

1.整体架构

ArrayList整体架构比较简单,是一个数组结构,
在这里插入图片描述
上图展示的是长度为10的数组(10也是ArrayList的默认最小初始化长度),ArrayList关键成员变量如下,

变量名说明
elementData数组本身
index数组索引
DEFAULT_CAPACITY数组初始大小,默认是10
size当前数组的大小,类型是int,未使用volatile修饰,所以是线程不安全的
modCount当前数组被修改的次数,也称为版本号,数组结构有变动就会+1

2.源码解析

初始化

初始化的方式有3种,

  • 无参数初始化
  • 指定大小初始化
  • 指定初始数据初始化

具体源码如下,

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.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 = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

	/**
	 * 无参数初始化
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

	/**
	 * 指定大小初始化
     * Constructs an empty list with the specified initial capacity.
     * @param  initialCapacity  指定列表长度
     * @throws IllegalArgumentException 当指定值是负数时报错
     */
    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);
        }
    }
    
    /**
     * 指定初始数据初始化
     * @param c 传入Collection对象
     * @throws NullPointerException 当传入的对象为null时报错
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[](这种情况比较少见)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else { // 当转换得到的数组长度为0时,replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
}
初始化方式说明
无参数初始化初始化一个空数组(起初并未完全初始化),当添加元素时才完全初始化,其长度为10
指定大小初始化当指定值为负数会报错;当指定值为0,默认引用EMPTY_ELEMENTDATA空数组;当指定值大于0,创建指定大小的数组
指定初始数据初始化调用集合对象的toArray方法得到数组,对数组长度及返回类型进行校验。如果数组长度为0,则elementData引用EMPTY_ELEMENTDATA空数组。

补充说明:

  • ArrayList无参数构造时,elementData引用的首先是一个空数组,而不是默认长度为10的空数组。只有在第一次调用add方法时才会扩容到10
  • 指定数据初始化时,有一条判断句elementData.getClass() != Object[].class,当给定集合中的元素类型不是Object类时,会转化为Object类型。但是未必会成功转化,这是Java的一个bug,很少会被触发。
    目前已知的会触发该bug的情景如下,
@Test
public void showBug() {
	List<String> list = Arrays.asList("hello world");
	Object[] objArray = list.toArray();
	System.out.println(objArray.getClass().getSimpleName());		// 输出 String
	objArray[0] = new Object();		// 报错
} 

这个bug在Java 9中被解决。

添加元素及扩容

这两个放在一起讲的原因是新增元素时会分为两步,

  • 判断是否需要扩容,如果需要则先对数组进行扩容
  • 元素赋值

完整过程如下,之后会逐步进行解析
Image

add方法解析

add方法源码,

/**
 * Appends the specified element to the end of this list.
 * @param e 添加到列表末尾的元素
 * @return 添加成功返回true
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

/**
 * 在指定位置添加元素(相当于插入)
 * @param index 添加元素的索引位置
 * @param element 添加的元素
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 将index位置之后的元素拷贝到新的位置
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 新元素放到index位置
    elementData[index] = element;
    size++;
}

上面是add方法的两种调用形式,分别用于队尾添加元素和指定位置添加元素。

ensureCapacityInternal方法解析

ensureCapacityInternal方法判断容量是否合适,若不合适则进行扩容操作,

private void ensureCapacityInternal(int minCapacity) {
	// 当elementData为无参数初始化时对应的数组,第一次调用add方法会先进行初始化。
	// 默认最小长度为10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

	// 这个方法才是真正确保容积足够
    ensureExplicitCapacity(minCapacity);
}

ensureCapacityInternal方法具体分为两步,

  • 首先对elementData进行判断,如果是无参数初始化生成的ArrayList且elementData还是DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,则首先进行初始化。最小长度默认为10。
  • 如果之前已初始化,则调用ensureExplicitCapacity确保此次调用add方法时数组容积足够。

ensureExplicitCapacity方法解析

ensureExplicitCapacity方法源码,

private void ensureExplicitCapacity(int minCapacity) {
	// 数组的修改次数+1(版本更新)
    modCount++;

    // 如果添加元素后长度超过当前容积,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

该方法的执行也分为两步,

  • 更新ArrayList对象elementData的版本
  • 判断当前容积是否足够,如果不足则调用grow方法扩容

grow方法解析

grow方法是实际对数组操作的方法,

/**
 * 扩容数组,使其至少能够包含minCapacity个元素
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // 使用位运算,右移一位(相当于oldCapacity/2)
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 确保数组能够容纳至少minCapacity个元素,同时不超过最大元素限制
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 调用Arrays工具类,扩容得到新的数组(该工具类底层调用系统方法)
    elementData = Arrays.copyOf(elementData, newCapacity);
}

grow方法的执行过程有3点需要注意,

  • 扩容的规则不是翻倍,增量是原来容量的一半,即变为1.5倍。
  • ArrayList数组默认的最大容纳量是Integer.MAX_VALUE个元素,超过该值则JVM不会再给分配空间。
  • 新增数据时并没有对值进行严格校验,ArrayList添加的新值可以为null。

数组大小溢出及线程非安全

add方法中有数组大小溢出的意识,即扩容后数组大小在[0,Integer.MAX_VALUE]这个区间范围内。自定义类型时,这种意识是值得借鉴的。

在add方法执行到最后时,直接在数组上添加元素,

elementData[size++] = e;

该过程没有任何锁龙之,在多线程操作时是线程不安全的

扩容的本质

grow方法中调用

Arrays.copyOf(element, newCapacity);

对数组进行扩容,本质上是数组之间的拷贝。具体分为两步,

  • 创建一个符合预期容量的新数组
  • 把老数组的元素拷贝到新数组中

Arrays.copyOf方法内调用的是底层的系统方法,

/**
 * @param src     被拷贝的数组
 * @param srcPos  从数组那里开始
 * @param dest    目标数组
 * @param destPos 从目标数组那个索引位置开始拷贝
 * @param length  拷贝的长度 
 * 此方法没有返回值,通过 dest 的引用进行传值
 */
System.arraycopy(Object[] src, int srcPos, Object[] dest, int destPos, int length);

删除元素

删除元素的具体过程如下,
Image
某个元素被删除后,为维护数组连续的结构,把删除元素之后的元素向前移动。

remove方法解析

/**
 * 删除数组中第一个出现的指定元素,如果元素本身就不存在,数组不会改变。
 * @param o 指定被删除的元素
 * @return 如果数组中存在该元素,返回true;反之,false
 */
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;
}

上面的代码中有两点需要注意,

  • 新增元素时没有对null进行校验,所以删除元素时允许删除null值。
  • 通过equals方法寻找元素的索引,如果元素不是基本类型,需要关注equals的具体实现。

fastRemove方法解析

该方法用于删除指定索引位置的元素,

/*
 * Private remove method that skips bounds checking and does not
 * return the value removed.
 */
private void fastRemove(int index) {
	// 更新数组的版本信息
    modCount++;
	// index位置之后的元素都要前移
    int numMoved = size - index - 1;
    if (numMoved > 0)
    	// 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //数组最后一个位置赋值 null,帮助 GC
    elementData[--size] = null;
}

迭代器(内部类Itr)

自定义迭代器只需要实现Java中java.util.Iterator接口即可,ArrayList也是这样做的。ArrayList类中存在iterator方法,

public Iterator<E> iterator() {
    return new Itr();
}

上面的代码中Itr类是ArrayList类的一个内部类,该类实现了Iterator接口,

private class Itr implements Iterator<E> {
	......
}

迭代器中有三个重要的参数,

类型参数说明
intcursor迭代过程中下一个元素的位置,默认从0开始
intlastRet新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
intexpectModCount=modCountexpectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。

迭代器一般都要有三个方法,

方法名说明
hasNext是否存在值能够迭代
next返回可以迭代的值
remove删除当前迭代的值

这三个方法都是Itr类的成员方法

hasNext方法解析

public boolean hasNext() {
	//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
	return cursor != size;
}

next方法解析

public E next() {
	//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
    checkForComodification();
    
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    // 下一次迭代时,元素的位置,为下一次迭代做准备
    cursor = i + 1;
    // 返回元素值,同时此处会更新lastRet的值
    return (E) elementData[lastRet = i];
}

next方法的执行过程分为两步,

  • 检查是否能够继续迭代
  • 找到迭代的值,并更新cursor为下一次迭代做准备
  • 更新lastRet的值为当前位置i,不更新的话该类的remove方法将永远只能抛出异常

checkForComodification方法用于比较版本号,

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

remove方法解析

public void remove() {
	// 如果lastRet小于0,说明数组该位置的元素已经被删除
    if (lastRet < 0)
        throw new IllegalStateException();
    
    //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
    checkForComodification();

    try {
    	// 此处是调用ArrayList的remove方法删除该位置元素
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        // -1 表示元素已经被删除,这里也防止重复删除
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

remove方法有两点注意,

  • lastRet = -1是为了在当前位置防止重复删除
  • 元素删除成功后,数组的modCount会更新,此处吧expectedModCount重新赋值,下次使用next方法进行迭代时二者的值是一致的

线程安全

当ArrayList在多线程中以共享变量的方式存在时才会有线程安全问题,当ArrayList是方法内部的局部变量时,是不存在线程安全问题的。

ArrayList线程非安全的原因是对elementData、size和modCount进行操作的过程中没有加锁,而且这些变量并不是volatile关键字修饰的(CPU不可见),所以当多线程同时操作时会出现值被覆盖的问题。

解决线程安全问题的方法是使用Collections.synchronizedList方法将原本的List对象包装为线程安全的SynchronizedList类对象。SynchronizedList通过在每个方法上加锁实现,但是降低了性能。

synchronizedList方法两种形式,

/**
 * Returns a synchronized (thread-safe) list backed by the specified list.
 * @param list 需要被改造成线程安全的List对象
 */
public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

/**
 * Collections工具类 synchronizedList方法
 * @param list 需要被改造成线程安全的List对象
 * @param mutex Object on which to synchronize(相当于锁对象)
 * @return 如果数组中存在该元素,返回true;反之,false
 */
static <T> List<T> synchronizedList(List<T> list, Object mutex) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list, mutex) :
            new SynchronizedList<>(list, mutex));
}

如果没有指定mutex参数,则默认使用Collections类中已经定义好的一个Object对象作为SynchronizedList类的锁。

其中SynchronizedList类是Collections类的内部类,

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {

该类的方法以add方法为例,

public void add(int index, E element) {
    synchronized (mutex) {
    	list.add(index, element);
    }
}

总结

ArrayList围绕底层数组结构,各个API都是对数组的操作进行封装。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值