ArrayList源码分析

数据结构

底层的数据结构就是数组,数组元素类型为Object类型

源码分析

一、类的继承关系

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

说明:ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。
接口讲解

二、成员变量

private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData; 
private int size;

private static final Object[] EMPTY_ELEMENTDATA = {};
/**用于默认大小的空实例的共享空数组实例。我们将其与空元素数据区分开,以知道何时充气添加第一个元素*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

成员变量 transient关键字

节省空间
因为elementData里面不是所有的元素都有数据,因为容量的问题,elementData里面有一些元素是空的,这种是没有必要序列化的。ArrayList的序列化和反序列化依赖writeObject和readObject方法来实现。可以避免序列化空的元素。

成员变量 2个空数组意义

//list1.elementData为EMPTY_ELEMENTDATA,
ArrayList list1 = new ArrayList(0);
//list2.elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ArrayList list2 = new ArrayList();

添加首个元素时,扩容区别
list1 扩容为 1
list2 扩容为 DEFAULT_CAPACITY = 10

线程安全

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内部局部变量时,是没有线程安全问题的。

本质: ArrayList 自身的 elementData、size、modCount 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见的(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

三、构造函数

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) { // 初始容量大于0
            this.elementData = new Object[initialCapacity]; // 初始化元素数组
        } else if (initialCapacity == 0) { // 初始容量为0
            this.elementData = EMPTY_ELEMENTDATA; // 为空对象数组
        } else { // 初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    public ArrayList() { 
        // 无参构造函数,设置元素数组为空 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
   
    public ArrayList(Collection<? extends E> c) { // 集合参数构造函数
        elementData = c.toArray(); // 转化为数组
        if ((size = elementData.length) != 0) { // 参数为非空集合
            if (elementData.getClass() != Object[].class) // 是否成功转化为Object类型数组
                elementData = Arrays.copyOf(elementData, size, Object[].class); // 不为Object数组的话就进行复制
        } else { // 集合大小为空,则设置元素数组为空
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

四、新增和扩容

新增主要分为两步:

  1. 判断是否需要扩容,如果需要则执行扩容操作;
  2. 赋值新元素。

add函数

public boolean add(E e) {
	// 确保数组大小是否足够,不够则执行扩容,size 为当前数组的大小
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 直接赋值
    elementData[size++] = e;
    return true;
}

扩容步骤(ensureCapacityInternal)源码:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算 capacity
private static int calculateCapacity(Object[] elementData, int minCapacity) {
	// 无参构造 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return 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;
    // 新的容量为旧的容量的 1.5 倍(capacity >> 1  ==> capacity / 2)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新的容量小于我们期望值 minCapacity,则将新的容量值赋值为期望值
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新的容量大于 JVM 所能分配的数组的最大值,那么就用 Integer 的最大值
    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. 扩容的规则并不是翻倍,而是 原来容量大小 + 原来容量大小的一半,直白来说,扩容后的大小是原来容量的 1.5 倍;
  2. ArrayList 中数组的最大容量是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了;
  3. 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。

扩容的本质
Arrays.copyOf(elementData, newCapacity)

public static int[] copyOf(int[] original, int newLength) {
    int[] copy = new int[newLength];
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);

五、删除和缩容

remove函数

public E remove(int index) {
        // 检查索引是否合法
        rangeCheck(index);
        
        modCount++;
        E oldValue = elementData(index);
        // 需要移动的元素的个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        // 赋值为空,有利于进行GC
        elementData[--size] = null; 
        // 返回旧值
        return oldValue;
    }

缩容的本质

public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);

数组复制几种方法效率链接

fail-fast 机制

fail-fast 机制是java集合(Collection)中的一种错误机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。
一种理念,在进行系统设计时优先考虑异常情况

什么时候会出现fail-fast?

以下两种情况下会导致fail-fast,抛出ConcurrentModificationException

  • 单线程环境
    遍历一个集合过程中,集合结构被修改。注意,listIterator.remove()方法修改集合结构不会抛出这个异常。
  • 多线程环境
    当一个线程遍历集合过程中,而另一个线程对集合结构进行了修改。

原理

// AbstractList中唯一的属性
// 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1
protected transient int modCount = 0;

// ArrayList 代码
final void checkForComodification() {
    if (expectedModCount != ArrayList.this.modCount)
          throw new ConcurrentModificationException();
    }

执行 checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

fail-fast 解决方式

使用java.util.concurrent包下的类去取代java.util包下的类
例如:private static List list = new ArrayList();
替换为:private static List list = new CopyOnWriteArrayList();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值