ArrayList底层原理浅析

一. ArrayList和LinkedList异同

1. 相同点:

     ArrayList 和LinkedList 都是List 接口下的实现类,相同于List接口,这两个实现类也同样是有序且不唯一(可重复)的集合类。

2. 不同点:

      ArrayList 的底层仍然是使用数组(Object[] elementData)实现的,通过对数组操作的封装,简化了程序员编程中对集合的使用过程。ArrayList是List接口的主要实现类,也就是使用得最多的,因为其效率高,但是线程不安全。
   LinkedList 不同于ArrayList,其底层使用了双向链表存储数据,实现集合功能。由于其是双向链表实现,对于集合插入、删除需求高的情况,建议使用LinkedList。

二. ArrayList的底层实现原理浅析

1. 部分成员变量及常量的作用
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
// 常量MAX_ARRAY_SIZE表示整型的最大值-8,文末会用到并解释作用
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

      这是在ArrayList 源码中的部分成员变量及常量,先对这几个解释以便后续理解。
     Object[] elementData:前面提到ArrayList 其实就是以数组存储数据的,elementData就是该存储数据的数组,后续对数组的数据操作就是对该数组的操作。

     private int size:size指的是该elementData的容量大小。

      final int DEFAULT_CAPACITY = 10:当使用ArrayList 的无带参构造方法初始化时,即无指定数组容量,会有一个默认的数组容量,就是DEFAULT_CAPACITY ,即数组容量默认是10。

   final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:该常量是一个空数组,当ArrayList 初始化时,若没有指定容量大小,即调用无带参构造函数,在底层会先将elementData初始化为空数组。

2. ArrayList的构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
    
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);
    }
}
  • 调用ArrayList 的无参构造函数时,会先将elementData 初始化为空数组,即赋值为常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
          值得注意的是,当ArrayList 初始化时,若没有指定容量大小,即调用无参构造方法,在底层会先将elementData初始化为空数组,而不是直接将elementData初始化为默认容量(10)。当开始对elementData进行存储数据的操作时,如调用add方法,才会将其容量拓展为默认容量(10),(若使用addAll方法添加元素集合,则初始化大小为10和添加集合长度的较大值)。
          为什么要提这一点呢,因为该情况是JDK1.8对ArrayList进行优化后才出现的,最初在JDK1.7及之前版本,对ArrayList 的初始化后,若无指定容量则直接将elementData初始化为默认容量(10)。
          相比于JDK1.7,JDK1.8的做法不会在初始化ArrayList 后就创建空间,而是等到真正需要将数据存入数组时才拓展容量,对内存占用有所优化。

  • 调用带参构造方法时,根据传入的int类型变量initialCapacity,initialCapacity即为指定的容量大小,将数组初始化为指定容量大小的数组。
    若传参initialCapacity为0,则进行相同于无参构造方法的操作,即elementData为空数组,当开始对elementData进行存储数据的操作时,将其容量拓展为默认容量(10)。
    若initialCapacity < 0,则抛出异常。

  • 除此之外ArrayList还有一个带参构造方法传入的参数是Collection<? extends E> c。该构造方法按照它们由集合的迭代器返回的顺序构造一个包含指定集合的元素的列表。
    源码如下:

public ArrayList(Collection<? extends E> c) {
   elementData = c.toArray();
   if ((size = elementData.length) != 0) {
       // c.toArray might (incorrectly) not return Object[] (see 6260652)
       if (elementData.getClass() != Object[].class)
           elementData = Arrays.copyOf(elementData, size, Object[].class);
   } else {
       // replace with empty array.
       this.elementData = EMPTY_ELEMENTDATA;
   }
}
3. ArrayList的add方法及容量拓展

      我们知道ArrayList 的底层数组elementData的容量要么是默认值(10),要么是指定容量。但是当我们对其进行添加数据超过了elementData的容量大小会怎么样呢?这时候就需要进行容量拓展的处理,前面说到的elementData初始化为空数组,等到对数据进行存储时才进行的容量拓展是也是类似的处理方式,只不过根据具体情况的不同会有些许步骤有所差别。
      注意:虽说是数组的容量拓展,但其实并不是扩大原有数组的大小,因为数组一旦定义大小是不可改变。所谓的容量拓展是以原有的数组容量为基础,根据具体情况新建一个符合条件容量的数组,并将原数组数据复制到该新数组,以此实现容量拓展。这在后面会提到。
      接下来看看其处理过程。

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

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

      容量拓展处理过程:此时先假设elementData容量大小n=10。当elementData的10个容量都已经存储了数据后,又有新的数据需要add()进来,此时调用ensureCapacityInternal(size + 1)方法,因为是要add()一个数据进来,所以传参是原本的数组大小+1,。在该方法中有一个if 语句

private void ensureCapacityInternal(int minCapacity) {
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
   }
   ensureExplicitCapacity(minCapacity);
}
// 别忘了前面提到DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

      这个if 语句存在的意义就是当elementData如果是初始化后的空数组时,就需要进行容量拓展,其拓展的容量就是默认容量10。这对应着上文提到的JDK1.8对ArrayList优化后的特点:若没有指定容量大小,即调用无参构造函数,在底层会先将elementData初始化为空数组,当开始对elementData进行add操作时,才会将其容量拓展为默认容量(10)。 将变量minCapacity设置为10后,调用ensureExplicitCapacity(minCapacity)。
      若elementData是非空数组,那么直接调用ensureExplicitCapacity(minCapacity)函数

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

      这个方法也有一条if 语句,表示的意思是当minCapacity 大于数组的长度时,则表明需要的数组容量已经比当前的数组容量大,因此需要进行容量拓展,而grow(minCapacity)方法就是对容量拓展的方法,将其调用。

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

      oldCapacity 表示当前数组的容量大小,oldCapacity + (oldCapacity >> 1)表示将当前容量大小拓展为1.5倍大小,即若原本容量大小n=10的话,拓展完后的容量n=15。由此可看出ArrayList正常情况下的容量拓展都是拓展为原来的1.5倍。拓展完的容量大小赋值给变量newCapacity后,接着进行判断,此时有两条if 语句:
      先看第一条if 语句:

 if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

      第一个条if 语句表示当拓展后的容量比原来的容量还小时的情况。可是为什么n拓展为原来1.5倍却比原来小呢?这时候就回到之前说的若没有指定容量大小,即调用无参构造函数,在底层会先将elementData初始化为空数组,当开始对elementData进行存储数据的操作时,才会将其容量拓展为默认容量(10)。 也就是说当elementData为空数组时,长度为0,0的1.5倍仍然为0,所以要将容量直接拓展为minCapacity,而minCapacity的值在此情况下就是默认容量10。这条if 语句处理了调用无参构造函数,将elementData初始化为空数组的容量拓展问题。
      接着看第二条if 语句:

if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
// 常量MAX_ARRAY_SIZE表示整型的最大值-8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

      当拓展后的容量大于MAX_ARRAY_SIZE而又小于整型数的最大值时,那么将容量设置为整型数的最大值Integer.MAX_VALUE。这个处理在 hugeCapacity(minCapacity)方法内进行。源码如下:

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 溢出
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

      当确定拓展容量的具体数值后,接着grow方法下面的内容:

elementData = Arrays.copyOf(elementData, newCapacity);

      这条语句调用了copyOf(elementData, newCapacity)方法,这就是之前提到的容量拓展不是扩大原有数组的大小,而是新建一个符合条件容量的新数组。
      在这些方法处理完后完成了容量拓展,就又回到了add()方法,此时接着执行add()的添加数据的操作,因为已经完成了容量拓展,对于新数据有了多余的容量存放。

      ArrayList 源码中还封装了对数组的各种操作的方法,建议可以去看看。本篇文章仅作为辅助理解ArrayList 部分源码。有错欢迎评论指出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值