深入Java集合ArrayList的源码解析

现在由大恶人付有杰来从增删改查几个角度轻度解析ArrayList的源码

首先ArrayList的底层数据结构非常简单,就是一个数组。
在这里插入图片描述
从源码第115行我们可以得出信息,他的默认数组长度是10。

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

那么我们经常调用的size方法是什么呢?
源码第281行,142行

 /**
     * Returns the number of elements in this list.
     *返回链表中元素的个数
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }
 /**
     * The size of the ArrayList (the number of elements it contains).
     *同上
     * @serial
     */
    private int size;

另外,还有一个关键的属性:

//modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1
 protected transient int modCount = 0;

以上表达的意思就是说,ArrayList的默认大小是10,内部记录有自己被修改的次数,和链表中有效的元素。所谓有效的元素就是你自己添加的元素。

1.构造方法

有三种构造方法:
在这里插入图片描述
1.指定大小初始化

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

清晰明了哈,如果指定的大小是大于0的,那么就用这个数字初始化,否则就初始一个空的数组。如果是非法输入(<0),就会抛出IllegalArgumentException异常。
2.无参构造函数初始化

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

我们可以看到,无参构造 函数并不是一来就 初始化了10个长的数组,而是初始化了一个空的数组。这样能够省点空间吧。面试官问起来了,初始化的大小是10码?绝对不是哈,是0。
3.指定数据初始化

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

这个代码的意思就是,凡是继承于Collection的,爷都能初始化。List接口继承自Collection的。
演示一下2种姿势

		 List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        ArrayList<Integer> arrayList = new ArrayList<>(list);
        ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(2, 4, 5, 6, 7, 8));

是不是很方便,如果你不知道这个方法,你还要手动去add 1 2 3 4 5.

2.新增和扩容实现

新增就是往数组中添加元素,主要分成两步:

  • 判断是否需要扩容,如果需要执行扩容操作;
  • 直接赋值。
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 public void add(int index, E element) {
 //判断索引是不是合法的
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

我们常用时第一种,第二种是在指定位置添加,把原来位置 的挤到后面去。后面的所有元素都要让一步,性能消耗会很大。
在添加元素之前,总是有一个

  //确保数组大小是否足够,不够执行扩容,size 为当前数组的大小
 ensureCapacityInternal(size + 1);

我们仔细想一想是吧,你添加元素,size就加1,所以就判断size+1是否满足。
ensureCapacityInternal(size + 1);做了很多事情。我把相关的代码都复制过来。

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, 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;
        //原来老旧的容量除以2,加上老的容量。实锤了!!1.5倍速扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容了,还是不够用,那么就用你声明的值。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //如果扩容了,大于Integer.MaxValue 就用Inter.maxValue
        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.5 倍;
int newCapacity = oldCapacity + (oldCapacity >> 1);
  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。
if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
  • 源码在扩容的时候,有数组大小溢出意识,就是说扩容后数组的大小下界不能小于 0,上界不能大于 Integer
    的最大值,这种意识我们可以学习。
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

扩容的本质:

扩容的本质就是新开了一个扩容的数组,然后把原来数组的元素批量赋值过去,最后修改内部的elementData引用。

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
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

** Arrays.copyOf是调用的 System.arraycopy,后者是本地方法**

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

3.删除

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,我们选取根据值删除方式来进行源码说明:
代码532行:

public boolean remove(Object o) {
  // 如果要删除的值是 null,找到第一个值是 null 的删除
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  } else {
    // 如果要删除的值不为 null,找到第一个和要删除的值相等的删除
    for (int index = 0; index < size; index++)
      // 这里是根据  equals 来判断值相等的,相等后再根据索引位置进行删除
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

我们需要注意的两点是:

  • 新增的时候是没有对 null 进行校验的,所以删除的时候也是允许删除 null 值的;
  • 找到值在数组中的索引位置,是通过 equals 来判断的,如果数组元素不是基本类型,需要我们关注 equals 的具体实现。(这个和 == 的区别,不懂自己去百度哈)
    然后看看里面的fastRemove(源码544行)
private void fastRemove(int index) {
  // 记录数组的结构要发生变动了
  modCount++;
  // numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
  // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
  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;
}

从源码中,我们可以看出,某一个元素被删除后,为了维护数组结构,我们都会把数组后面的元素往前移动(所以说,数组的删除性能开销真的很大)

4.迭代

如果要自己实现迭代器,实现 java.util.Iterator 类就好了,ArrayList 也是这样做的(内部类的方式),我们来看下迭代器的几个总要的参数:

int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。
int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。

在这里插入图片描述
迭代器一般来说有三个方法:

  • hasNext 还有没有值可以迭代
  • next 如果有值可以迭代,迭代的值是多少
  • remove 删除当前迭代的值
public boolean hasNext() {
  return cursor != size;//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代
}
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;
  // 返回元素值
  return (E) elementData[lastRet = i];
}
// 版本号比较
final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}

从源码中可以看到,next 方法就干了两件事情,第一是检验能不能继续迭代,第二是找到迭代的值,并为下一次迭代做准备(cursor+1)。

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

  try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    // -1 表示元素已经被删除,这里也防止重复删除
    lastRet = -1;
    // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
    // 这样下次迭代时,两者的值是一致的了
    expectedModCount = modCount;
  } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
  }
}
  • 删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 重新赋值,下次迭代时两者的值就会一致了

其他:

都说数组的添加元素 的时间复杂度是O(1),真的如此吗?

如果我们直接调用Add(x)的方法,且数组容量足够这个数组挂在后面,那么时间复杂度就是1,如果触发了扩容机制,那么就是O(N),在使用add(index,e)的时候,时间复杂度一般来说不是O1,因为要移动索引 后面的元素。

什么是falilFast的机制?
在遍历过程中,如果数据被修改,就会报错。

 Iterator<Integer> iterator = arrayList.iterator();
        new Thread(new Runnable() {
            @Override
            public void run() {
                arrayList.remove(2);
            }
        }).start();
        while (iterator.hasNext()){
            Thread.sleep(50);
            System.out.println(iterator.next());
        }
        //同上
        for (Integer integer : arrayList) {
            System.out.println(integer);
        }

在这里插入图片描述
上面提到,遍历的时候会记录modCount的值,如果和自己期望的不一样,就会报错。
fail-fast解决办法
方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

数组初始化,被加入一个值后,如果我使用 addAll 方法,一下子加入 15 个值,那么最终数组的大小是多少?
分析:在加入一个元素的时候,数组被初始化成10个,然后一下子加入15个,那么就会触发扩容,在初次扩容后,大小变成了15(1.5倍速度扩容),发现 还是不够用,就会使用1+15这个值作为容量。所以答案是16.
再贴一遍源码

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //原来老旧的容量除以2,加上老的容量。实锤了!!1.5倍速扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容了,还是不够用,那么就用你声明的值。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //如果扩容了,大于Integer.MaxValue 就用Inter.maxValue
        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);
    }

现在我有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?
因为原数组比较大,如果新建新数组的时候,不指定数组大小的话,就会频繁扩容,频繁扩容就会有大量拷贝的工作,造成拷贝的性能低下,所以回答说新建数组时,指定新数组的大小为 5k 即可。
所以大恶人付有杰建议,平常自己心知肚明的时候,自己手动指定大小。

还有ArrayList删不干净的问题:

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,2,2,2,3));
        for(int i = 0;i<list.size();i++){
            if(list.get(i).equals(2)){
                list.remove(i);
            }
        }
        System.out.println(list.toString());

代码输出:
在这里插入图片描述
兄弟们自己去想吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章