【JDK源码】ArrayList

简介

  • ArrayList继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化。允许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。

ArrayList的常量与变量

在这里插入图片描述

  • ArrayList 底层是基于数组来实现容量大小动态变化的。
// 存放当前数据,不参与序列化
transient Object[] elementData; // non-private to simplify nested class access

// 元素个数大小
private int size;

上面的 size 是指 elementData 中实际有多少个元素,而 elementData.length 为集合容量,表示最多可以容纳多少个元素。

  • 默认初始容量大小为 10
// 序列ID
private static final long serialVersionUID = 8683452581122892189L;

// ArrayList默认的初始容量大小
private static final int DEFAULT_CAPACITY = 10;
  • 两个空数组,有什么区别呢?
// 空对象数组,用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {
    
    };

// 空对象数组,如果使用默认的构造函数创建,则默认对象内容是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
    
    };

在这里插入图片描述

简单来说: 调用无参构造是用Default,调用有参构造时如果初始长度为0则用Empty

当集合中的元素超出数组规定的长度时,数组就会进行扩容操作,扩容操作就是ArrayList存储操作缓慢的原因,尤其是当数据量较大的时候,每次扩容消耗的时间会越来越多

ArrayList的构造方法

ArrayList()

/**
* Constructs an empty list with an initial capacity of ten.
*/
// 默认的构造方法,构造一个初始容量为10的空列表
public ArrayList() {
    
    
    // elementData 初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

注意:注释是说构造一个容量大小为 10 的空的 list 集合,但构造函数只是给 elementData 赋值了一个空的数组,其实是在第一次添加元素时容量扩大至 10 的,在grow源码里面里面

ArrayList(int initialCapacity)

  • 所以当我们要使用ArrayList时,可以 new ArrayList(大小)构造方法来指定集合的大小,以减少扩容的次数,提高写入效率,该构造函数的源码如下:

在这里插入图片描述

// 自定义初始容量的构造方法
public ArrayList(int initialCapacity) {
    
    
    if (initialCapacity > 0) {
    
    
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    
    
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
    
    
        // 如果初始容量小于0,则会出现 IllegalArgumentException 异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
}

ArrayList(Collection c)

  • 将 Collection 转化为数组并赋值给 elementData,把 elementData 中元素的个数赋值给 size。 如果 size 不为零,则判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换。 如果 size 为零,则把 EMPTY_ELEMENTDATA 赋值给 elementData,相当于new ArrayList(0)。

在这里插入图片描述

// 构造一个包含指定元素的列表集合,按集合的返回顺序迭代器
// 传入参数为Collection对象
// c要将其元素放入此列表的集合
public ArrayList(Collection<? extends E> c) {
    
    

    // 调用toArray()方法将Collection对象转换为Object[]
    elementData = c.toArray();

    // 判断size的大小,如果size值为0,则会抛出NullPointerException异常
    // 如果size > 0 ,则执行以下代码
    if ((size = elementData.length) != 0) {
    
    
        //判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
    
    
        // 相当于new ArrayList(0)
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList的方法

add()

  • 首先会判断是否需要扩容

在这里插入图片描述

//参数 :需要添加的元素,需要添加的数组,数组元素的个数
private void add(E e, Object[] elementData, int s) {
    
    
    //如果数组长度等于元素个数则需要扩容
    if (s == elementData.length)
        elementData = grow();
    //否则直接赋值
    elementData[s] = e;
    size = s + 1;
}
public boolean add(E e) {
    
    
    //将 modCount 增加一(迭代时,将 expectedModCount 设为 modCount,若迭代过程中对集合进行了操作,易知 
    //expectedModCount  与 modCount 数值不等,则会抛出 ConcurrentModificationException 异常)
    //这个参数是指当前列表的结构被修改的次数
    modCount++;
    add(e, elementData, size);
    return true;
}
  • 扩容方法 grow 一般情况的扩容是1.5倍,如果期望长度大于1.5倍扩容长度 则直接用期望长度进行扩容

在这里插入图片描述

//参数:数组元素个数加一
private Object[] grow(int minCapacity) {
    
    
    //原来的数组长度
    int oldCapacity = elementData.length;
    //如果原来数组为空,或者初始化为new ArrayList(0) 则直接返回一个大小为10的默认数组 
    //这也就是上面说的第一次添加元素时容量扩大至 10 
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    
        //扩容调用ArraysSupport.newLength方法  将 原来数组长度,元素个数加一 减 数组长度(也就是1),原来数组长度的一半。传入
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                                                  minCapacity - oldCapacity, /* minimum growth */
                                                  //异或运算 减半
                                                  oldCapacity >> 1           /* preferred growth */);
        //扩容后的长度 和原来的元素 复制给elementData
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
    
    
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

private Object[] grow() {
    
    
    return grow(size + 1);
}
  • ArraysSupport.newLength() 扩容

在这里插入图片描述

//参数:原来数组长度,元素个数加一-数组长度(也就是1),原来数组长度的一半
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    
    

    //如果原数组长度一半小于了期望的长度 则直接用期望的长度 比如 原数组长度为1 期望1 一半为0.5 所以扩容为 2
    int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
     如果新数组的长度小于可分配数组的最大值 直接返回
    if (newLength - MAX_ARRAY_LENGTH <= 0) {
    
    
        return newLength;
    }
    //否则调用hugeLength 把原来数组长度和1传入
    return hugeLength(oldLength, minGrowth);
}

private static int hugeLength(int oldLength, int minGrowth) {
    
    
    int minLength = oldLength + minGrowth;
    if (minLength < 0) {
    
     // overflow
        throw new OutOfMemoryError("Required array length too large");
    }
    if (minLength <= MAX_ARRAY_LENGTH) {
    
    
        return MAX_ARRAY_LENGTH;
    }
    //将扩容长度设置为Interger.MAX_VALUE,也就是int的最大长度 如果超出则报堆内存异常
    return Integer.MAX_VALUE;
}

在这里插入图片描述

在这里插入图片描述

总结:

  • 在进行 add 操作时先判断下标是否越界,是否需要扩容,如果需要扩容,就复制数组,然后设置对应的下标元素值

  • 扩容:默认扩容一半,如果扩容一半不够的话,就用目标的size作为扩容后的容量

get()

  • 先判断索引越界,再寻值

在这里插入图片描述

// 先判断下标索引
public E get(int index) {
    
    
    // 调用rangeCheck判断是否超出了Object数组长度
    Objects.checkIndex(index, size);
	// 调用 elementData 方法
	return elementData(index);
}
  • rangeCheck判断是否超出了Object数组长度 底层就是一个if语句

在这里插入图片描述

  • elementData(index)方法

在这里插入图片描述

// 通过下标索引找到对应的元素值,返回指定元素
E elementData(int index) {
    
    
    return (E) elementData[index];
}

set()

  • 首先也是判断是否越界

在这里插入图片描述

public E set(int index, E element) {
    
    
    // 调用rangeCheck判断是否超出范围
    Objects.checkIndex(index, size);
    // 返回指定元素,上面也讲到过
    E oldValue = elementData(index);
    elementData[index] = element;
    //返回修改前的值
    return oldValue;    
}

set方法返回的是原来的值oldValue

remove()

  • 通过底层数组索引删除

在这里插入图片描述

public E remove(int index) {
    
    
    //越界
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    //将需要删除的后面的元素全部向前移动一个元素位置,然后将最后一个元素(等于前一个元素)赋值为 null
    fastRemove(es, index);
	//返回删除前的值
    return oldValue;
}
  • fastRemove

在这里插入图片描述

private void fastRemove(Object[] es, int i) {
    
    
    //这个参数是指当前列表的结构被修改的次数
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        // 通过 System.arraycopy 方法将删除后面的元素往前移动一位
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}
  • 通过元素值删除

在这里插入图片描述

public boolean remove(Object o) {
    
    
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    //循环遍历找到要删除的值
    found: {
    
    
        //删除的值为空
        if (o == null) {
    
    
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
    
    
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        //说明没有要删除的值
        return false;
    }
    //同理
    fastRemove(es, i);
    return true;
}

ArrayList线程不安全

  • 并发环境下进行add操作时可能会导致elementData数组越界
问题现场如下:
有两个线程:t1,t2。有ArrayList size=9(即其中有9个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为9,判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为9,判断容量是否需要扩容
t1发现自己的需求为size!=10,容量足够,无需扩容
t1发现自己的需求为也size!=10,容量足够,无需扩容
t1开始设置元素操作,elementData[s] = e;size = s + 1;,成功,此时size变为10
t2也开始进行设置元素操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常:ArrayIndexOutOfBoundsException
  • 一个线程的值覆盖另一个线程添加的值

    • size大小符合预期,但是中间有null值存在
    问题现场如下:
    有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10
    t1进入add()方法,这时获取到size值为5,判断容量是否需要扩容
    t2也进入add()方法,这时获取到size值也为5,判断容量是否需要扩容
    t1发现自己的需求为size!=6,容量足够,无需扩容
    t1发现自己的需求为也size!=6,容量足够,无需扩容
    t1开始设置元素操作,elementData[s] = e,成功,
    t2也开始设置元素操作,elementData[s] = e,成功,注意此时t1的size = s + 1还没执行
    t1 size = size + 1 = 6,并写入主存
    t2 size = size + 1 = 7
    这样,size符合预期,但是t2设置的值被覆盖,而且索引为6的位置将永远为null,因为size已经为7,下次add()也会从7开始。除非手动set值。
    
    • size大小比预期的小
    问题现场如下:
    有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10
    t1进入add()方法,这时获取到size值为5,判断容量是否需要扩容
    t2也进入add()方法,这时获取到size值也为5,判断容量是否需要扩容
    t1发现自己的需求为size+1=6,容量足够,无需扩容
    t1发现自己的需求为也size+1=6,容量足够,无需扩容
    t1开始设置元素操作,elementData[s] = e,成功,
    t2也开始设置元素操作,elementData[s] = e,成功,注意此时t1的size = s + 1还没执行//也会有null
    t1 size = size + 1 = 6,暂未写入主存
    t2 size = size + 1 此时因为t1操作完size还未写入主存,所以size依然为5+1后仍为6
    t1将size=6 写入主存
    t2将size=6 写入主存
    这样,size=6 比预期结果小了。
    

猜你喜欢

转载自blog.csdn.net/qq_51998352/article/details/121025411