Java基础 ArrayList源码分析 JDK1.8

一、概述

  本篇文章记录通过阅读JDK1.8 ArrayList源码,结合自身理解分析其实现原理。

  ArrayList容器类的使用频率十分频繁,它具有以下特性:

  • 其本质是一个数组,因此它是有序集合
  • 通过 get(int i) 下标获取数组的指定元素时,时间复杂度是O(1)
  • 通过 add(E e)插入元素时,可直接向当前数组最后一个位置插入(这个描述不是特别准确,其中涉及到扩容、后续将讲解),其时间复杂度为O(1)
  • 通过 add(int i, E e)向指定位置插入元素时,是在原数组的基础上通过拷贝和偏移量实现,其时间复杂度为O(n)
  • 通过 remove(int i) 删除指定位置的元素时,同样也是在原数组的基础上通过拷贝和偏移量实现,其时间复杂度为O(n)
  • 使用内部类Itr()迭代删除ArrayList删除元素时,需使用迭代器的remove()方法,不能使用ArrayList.remove(Object o)方法
  • ArrayList是非线程安全的,可能造成数据丢失,数组越界的问题

二、源码分析

1.重要属性

private static final int DEFAULT_CAPACITY = 10;  //  扩容因子默认为10

transient Object[] elementData;            // 元素数据

private int size;                    //  当前ArrayList中实际元素数量

protected transient int modCount = 0;        //  ArrayList被修改的次数,这个属性是从java.util.AbstractList继承下来的

   

2.重要操作

  构造器

/*  
 * 无参构造器 将elementData初始化为一个空的数组
*/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/*
 * int整形构造器,将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);
    }
 }

 

  boolean add(E e)  添加一个元素

 将添加数组分解成以下5个步骤

//  添加一个元素时 必须确保当前数组可以添加一个新的元素,因此会根据capacity去计算
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    
    // ⑤ 将新元素添加到 扩容之后的数组size++坐标上,(注意是size++,先赋值再自增size)
    elementData[size++] = e;
    return true;
}


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


// ① 计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    /* 如果初始化ArrayList时 是使用无参构造 new ArrayList()
那么第一次向ArrayList添加元素时会将容量设为DEFAULT_CAPACITY 10个*/
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // ② 确认是否扩容 private void ensureExplicitCapacity(int minCapacity) { // ArrayList被修改次数 + 1 modCount++; /*
如果计算出来的容量 > elementData数组的长度时,那么会要扩容 因为elementData数组放不下新元素了;
否则的话就不需要扩容
*/ if (minCapacity - elementData.length > 0) grow(minCapacity); } // ③ 扩容 private void grow(int minCapacity) { // 新数组容量 = 老数组容量 * 1.5 int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); /* 但是必须要要保证 新容量 > ①中【计算出来的容量】 ,否则依然放不下新元素 如果出现这种情况,那么使用①中【计算出的容量】最为新数组容量,保证能放下元素 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 数组元素过多 使用Integer.MAX_VALUE 作为容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // ④ 通过拷贝的方式扩容 调用native层方法 elementData = Arrays.copyOf(elementData, newCapacity); }

 

  举个例子

 

// 往ArrayList中插入11个元素
List<Object> list = new ArrayList<>();
for (int i = 0; i < 11; i++) {
    list.add(new Object());
}

 上述代码很简单,步骤为:

  1. 调用无参构造器,初始化一个ArrayList
  2. 循环向该ArrayList插入11个元素

以该步骤为例:

  List<Object> list = new ArrayList<>();  //   初始化elementData为一个空数组{} , elementData.length = 0

  i = 0 时

  list.add(new Object());

  这个操作会经历上面代码片段的①②③④⑤步骤

  1. 在走步骤①时使用默认容量 10
  2. 走步骤②时  判断10 > elementData.length  因此使用10作为最小容量请求扩容
  3. 走步骤③时 elementData.length 扩大1.5倍后依然为0 因此使用10作为新容量
  4. 通过步骤④native层拷贝方法进行数组拷贝
  5. 通过步骤⑤在新数组的size++ (即第个0个坐标上),并且size自增为1

 

  当 1 ≤ i ≤ 9时 

   list.add(new Object());

  在步骤①算计容量时,minCapacity依次计算出来为 1, 2, 3, 4, 5, 6, 7, 8, 9 都小于elementData.length(当前经历了第一次扩容 为10) ,因此不会考虑扩容,直接将新增元素放到指定的下标中

  ps: 此步骤modCount每次都会+1

  当 i = 9时, 数组被填充满

 

 

  当 i = 10 时

  list.add(new Object());

  此时就不能将第11个元素放到数组中了,需要第二次扩容

  1. 执行代码片段步骤①,算出minCapacity=11
  2. 步骤②判断出minCapacity(11) > elementData.length(10),因此需要扩容
  3. 步骤③,通过在elementData.length基础上扩大1.5倍 即 15, 同时大于 minCapacity, 因此使用15作为新容量
  4. 步骤④,以15作为新数组容量,拷贝原数组到新数组中
  5. 步骤⑤,将新的元素放到扩容之后新数组下标10的位置中,并且size自增

 

  

可以看到,上述操作过程中list经历了两次扩容,因此在使用ArrayList的时候可以考虑使用有参构造器,确认ArrayList的大小,防止扩容发生,影响效率

 

    add(int i, E e)  向指定位置添加一个元素

public void add(int index, E element) {
    //    检查下标是否越界
    rangeCheckForAdd(index);

    //    计算容量 考虑是否扩容
    ensureCapacityInternal(size + 1);
    
    //    通过拷贝数组的方式插入
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}


private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

可以看到向指定位置插入元素和add(E e)方法步骤差不多,只是拷贝方式做了改变

  1. 不发生扩容时插入过程

  2. 发生扩容时插入过程

    

    E remove(int index)  根据下标删除元素

public E remove(int index) {
    //    检查索引是否越界
    rangeCheck(index);

    //    修改次数 +1    
    modCount++;
    //    取出需要删除的元素
    E oldValue = elementData(index);

    //    判断如果是删除中间的元素,则先自身向前拷贝的再删除;如果删除最后一个元素,则直接删除
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //    删除最后一个元素
    elementData[--size] = null; // clear to let GC do its work

    //    返回被删除的元素
    return oldValue;
}

 

删除元素时,是没有缩容操作的,例如一个ArrayList中elementData仅有10个元素并且存满了,remove掉9个元素后,size大小是1,但是elementData.length依旧为10

   

    void trimToSize()  修整大小

//    这个方法可以以当前elementData数组的size实际大小 重新拷贝出一个数组
//    防止上面问题的发生
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

 

 

    E get(int index)  从指定索引获取元素

//    获取指定元素
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

    这个方法很简单,检查完索引范围,直接从elementData数组上去取对应位置的元素

 

    boolean contains(Object o)  查询ArrayList中是否包含某个元素

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}


//    遍历elementData数组 查询满足条件的下标
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

可以看到查询ArrayList中是否包含某个元素时,是通过遍历整个数组,依次查询,因此该方法时间复杂度为O(n)

    

    Iterator<E> iterator()  迭代

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

//    内部类 迭代器
private class Itr implements Iterator<E> {
    int cursor;       // 返回数据的游标
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;    //    迭代器期望的被修改的次数 与ArrayList.modCount相同,

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //    ①  expectedModCount != modCount 时抛出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];
    }

    //    通过迭代器进行删除,每删除一个元素时expectedModCount会被重新赋值
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

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

前文中多次提到ArrayList的属性modCount,内部类Itr中提供的remove方法,在删除元素成功后,会将expectedModCount进行更新,因此,通过迭代器迭代ArrayList时,不能使用ArrayList.remove(Object o)方法,如果直接使用ArrayList.remove(Object o)进行删除(这个方法会对modCount减1),会导致迭代器进行下一次迭代时调用next()方法 检查到expectedModCount与modCount不同(上述代码①处),抛出ConcurrentModificationException,下面的代码示例将展示正确与错误在迭代中删除元素的操作

//    正确操作
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.next();    //    next()操作会更新游标cursor
    iterator.remove();
}


//    错误操作:将抛出ConcurrentModificationException
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer next = iterator.next();
    list.remove(next);
}

 

3.线程安全问题(非线程安全)

/*  size++非原子操作  elementData[size++] = e  等同于
  elementData[size] = e;
  size = size + 1  */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

 

  ArrayList是非线程安全的,原因是在add操作时,由于size++是非原子操作,多个线程去对同一ArrayList执行add(E e)方法时,会出现下面两种情况

 

  1.情况1:当两个线程同时进入add()方法,读取到当前size为0,并都往0的下标存元素时,就会导致其中的一个值被覆盖掉的问题


  2.情况2:假设当前ArrayList为elementData[10],可存放10个元素,并且当前已经存了9个元素了,那么此时size=9

情况1
线程A 线程B
add(e1) add(e2)
读到size为0 读到size也为0
elementData[0] = e1  
  elementData[0] = e2

 

 

 

情况2
线程A 线程B
add(e1) add(e2)
读到size为9 不需要扩容 读到size为9 不需要扩容
elementData[9] = e1  

size=size+1//size = 10

 
 

elementData[10] = e2

// 由于没有扩容导致数组越界

... 抛出异常了

 

 

 

posted @ 2019-05-29 20:32  __Sherry  阅读(233)  评论(0编辑  收藏  举报