搞懂Java集合框架之:01.ArrayList源码分析

ArrayList源码分析

  • 集合框架概览
  • ArrayList继承结构
  • ArrayList成员变量
  • ArrayList对象的创建
  • 自动扩容
  • 核心方法分析
  • Fail-Fast机制
  • 应用中的总结
    • 与LinkedList的遍历比较
    • 与数组的比较和取舍
    • 四种遍历方式
  • 常见面试题总结
  • 参考资料

在我们平时的开发过程中,ArrayList基本上是使用最多的一个集合类,它本质上是一个可以自动扩容的动态数组。为了一探ArrayList的究竟和实现原理,也为了学习高手的代码以提升自己的抽象设计能力,本篇博客深入ArrayList源码,对其具体实现如扩容机制、核心方法等做了一个简单分析。在博客的最后,对自己在使用过程中的一些取舍如遍历方法的选择、与数组的对比等做了一个总结,同时附上了一些常见的面试题。梳理自己知识结构的同时也将这个结果分享出来同大家一起进步,有疏漏之处还希望见者不吝赐教。
搞懂Java集合框架之:01.ArrayList源码分析_第1张图片
概括地说,ArrayList 的底层是一个动态数组,它是线程不安全的,允许元素为null,容量不足时支持以1.5倍的大小自动扩容。

并发场景下可以考虑用Vector或java.util.concurrent.CopyOnWriteArrayList来替代,具体可以结合使用场景与集合特性进行选型,后续的博客也会给出相应的分析和总结。

  • Vector:通过在方法前加synchronized关键字解决线程安全的问题,是锁同步机制,简单粗暴,性能较差
  • CopyOnWriteArrayList:适宜读多写少的并发场景,基于写入时复制(CopyOnWrite)思想解决线程安全问题

集合框架概览

ArrayList是Java集合框架中的一个集合类,为了更好的理解ArrayList的源码,我们首先需要对Java的集合框架有一个整体概览,下图便是Java集合框架的架构图,我们可以看到ArrayList、LinkedList、Vector等集合类都通过继承AbstractList实现了List接口,由此我们得知ArrayList是Java几大集合类别(List、Set、Map、Quene)中List集合的一个具体实现,另外一个同样很常用的实现类是LinkedList,在后续的博客中也会给出相应的源码分析。
搞懂Java集合框架之:01.ArrayList源码分析_第2张图片

ArrayList继承结构

在对ArrayList在Java集合框架中所处的位置有了一个概观的了解之后,我们来详细研究一下它的继承结构,如下图所示。我们可以发现ArrayList继承了AbstractList抽象类,实现了List、RandomAccess、 Cloneable、 java.io.Serializable等接口。由于是首次分析Java集合框架的源码,下面分别对这些接口和抽象类做一个简单介绍。
搞懂Java集合框架之:01.ArrayList源码分析_第3张图片

  • AbstractList:对集合类的共性代码做了抽象
  • List:List集合类型的顶层接口
  • RandomAccess:标识着其支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的
  • Cloneable:标识着其可以被复制,从代码可知ArrayList的复制是浅复制
  • java.io.Serializable:该接口主要是在序列化的时候起作用

ArrayList成员变量

下面我们正式进入到ArrayList源码的分析中,首先是其成员变量,核心是elementData和size,elementData用于存储容器元素,size表示容器当前存储的数据量。

由于elementData是Object[]类型的,所以ArrayList无法存储基本数据类型,对于基本数据类型的操作会有额外的Autoboxing、Unboxing操作,有一定的性能损耗,如果程序对性能要求较高可以考虑用数组替代之。

    // 默认容量是10
    private static final int DEFAULT_CAPACITY = 10;
    
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // ArrayList底层是基于数组实现的这句话便体现在此
    transient Object[] elementData; 
    
    private int size;
    
    // 继承自AbstractList,用于fail-fast错误检测机制
    protected transient int modCount = 0;

ArrayList对象的创建

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

    // 容器的默认初始容量不是10吗,怎么赋值了一个空数组呢?
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

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

上述代码便是ArrayList的三个构造方法,两个带参构造方法容易理解,不做过多说明。但在阅读无参构造方法时我们会产生一个疑问,容器的默认初始容量10体现在哪呢?代码中明明是赋值了一个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA啊,这里让人很困惑。别着急,这个问题的答案在我们阅读add()方法,并理解了扩容机制之后便显而易见了。我们发现他是将生成默认初始容量大小的数组这个逻辑延迟到添加第一个元素时做了实现,这样做有一个明显的好处是可以减少扩容的次数,从而提升程序性能,具体代码体现在calculateCapacity这个方法,引述如下,更深入的理解可以查看自动扩容这部分内容。

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 如果容器对象是通过无参构造方法创建的,并且第一次添加元素时所需容量不超过默认初始容量,则创建一个长度为10的数组用来存储数据。
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

自动扩容

ArrayList的扩容逻辑在源码中表达的很清晰,这里我们只将方法调用顺序自顶向下整理后的源码摘录如下,不做具体说明,相信认真阅读后自可明了。

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 需要保存的数据量大于现有数组长度时需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 1.5倍的扩容容量
        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 = 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;
    }

数组的扩容是新建一个大容量(原始数组大小+扩充容量)的数组,然后将原始数组数据拷贝到新数组,然后将新数组作为扩容之后的数组。数组扩容的操作代价很高,我们应该尽量减少这种操作。

从代码中可以看出每次扩容是以当前容量1.5倍的大小进行扩容的。

ArrayList扩容时会执行大量的数据搬移操作,时间复杂度较高,为了提高程序性能,在初始化容器时应该尽量合理估算可能的数据量以便于设置其初始容量。

ArrayList中数组的拷贝主要是通过调用方法Arrays.copyOf(elementData, size)或者直接调用本地方法System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength))实现的,进行的是浅拷贝,需要特别注意,如下对浅拷贝和深拷贝的概念做了一个简单介绍。

  • 浅拷贝:只拷贝对象引用,指向相同的内存空间,对象还是一个,更改对象的属性,拷贝前后的对象属性均会发生改变,浅拷贝的基本规则如下:

    1. 基本类型:如果变量是基本类型,则拷贝其值,比如int、float等。

    2. 对象:如果变量是一个实例对象,则拷贝其地址引用,也就是说此时新对象与原来对象是公用该实例变量。

    3. String字符串:如果变量为String字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有字符串对象保持不变。

  • 深拷贝:拷贝对象属性,构造一个全新的对象出来,指向不同的内存空间,更改其中一个对象的属性,不会引起另一个的变化。

核心方法分析

  • add、addAll
    // 将指定的元素追加到集合的末尾
    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++;
    }
    
    // 按照指定集合的迭代器返回的顺序,将指定集合中的所有元素追加到此集合的末尾
    // 如果在操作进行过程中修改了指定的集合,则此操作的行为未定义
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
    
    // 从指定位置开始,将指定集合中的所有元素插入此集合
    // 将当前位于该位置的元素(如果有)和任何后续元素向右移动(增加其索引)
    // 新元素将按照指定集合的迭代器返回的顺序出现在集合中
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }
  • remove、removeAll
    // 删除集合指定位置的元素,之后的元素整体前移一位
    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);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    // 如果集合中存在该元素,则删除这个元素,之后的元素整体前移一位;否则不做改变
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
    private void fastRemove(int index) {
        modCount++;
        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
    }
    
    // 将与指定集合中重叠的元素全部删除
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
  • clear
    // 清空集合,GC回收集合元素
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }
  • contains、indexOf、lastIndexOf
    // 判断集合元素是否存在
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    
    // 获取集合中元素出现的首个索引
    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;
    }
    
    // 获取集合中元素出现的最后一个索引
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
  • clone
    // 集合的浅拷贝
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
  • forEach、iterator
    // JDK1.8新特性
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    
    // 返回该集合元素的迭代器
    public Iterator<E> iterator() {
        return new Itr();
    }
  • sort
    // 对集合进行排序
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }
    
    // Arrays中的具体方法,具体涉及到的排序算法需要进一步研究???
    public static <T> void sort(T[] a, int fromIndex, int toIndex,
                                Comparator<? super T> c) {
        if (c == null) {
            sort(a, fromIndex, toIndex);
        } else {
            rangeCheck(a.length, fromIndex, toIndex);
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, fromIndex, toIndex, c);
            else
                TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
        }
  • toArray、subList、trimToSize
    // 集合转为数组
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    
    // 返回从from到to之间的列表,含from不含to,通过内部类的方式实现可以进一步研究这样实现的原因???
    public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
    }

    static void subListRangeCheck(int fromIndex, int toIndex, int size) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > size)
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
    }
    
    // 修改当前实例的容量为容器的当前大小
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

Fail-Fast机制

Fail-Fast是Java集合的一种错误检测机制。当遍历集合的同时修改集合或者多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制,记住是有可能,而不是一定。其实就是抛出ConcurrentModificationException 异常。
集合的迭代器在调用next()、remove()方法时都会调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。modCount是在每次改变集合数量时会改变的值。

具体代码如下:

    // ArrayList内部类Itr中
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

应用中的总结

与LinkedList的遍历比较

  • ArrayList:具有随机访问的特性,for循环遍历效率高
  • LinkedList:底层是双向链表,Iterator和for-each遍历效率高

与数组的比较和取舍

ArrayList的优势:

  • 可以将很多数组操作的细节封装起来
  • 支持动态扩容(1.5倍)

扩容操作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小。

数组的优势:

  • Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
  • 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
  • 当要表示多维数组时,用数组往往会更加直观。比如 Object[][] array;而用容器的话则需要这样定义:ArrayList > array。

总结:
对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

四种遍历方式

public class ArrayListTest {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList(Arrays.asList(1, 2, 3, 4, 5));
        // 第一种方式 for循环
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        // 第二种方式 for each
        for (Object o : list) {
            System.out.println(o);
        }
        // 第三种方式 迭代器
        Iterator it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
        
        // 第四种方式 forEach+Lambda表达式
        list.forEach((n) -> System.out.println(n));
    }
}

常见面试题总结

  1. ArrayList的大小是如何自动增加的?
  2. 什么情况下你会使用ArrayList?什么时候你会选择LinkedList?
  3. ArrayList 的插入删除一定慢么?
  4. ArrayList 的默认数组大小为什么是10?
  5. ArrayList 做队列合适么?
  6. ArrayList 中的 elementData 为什么是 Object 而不是泛型 E ?
  7. ArrayList list = new ArrayList(20); 中的list扩充几次?
  8. ArrayList 底层实现就是数组,访问速度本身就很快,为何还要实现 RandomAccess ?

参考资料

五道ArrayList面试题

面试题之—ArrayList实现原理

CopyOnWriteArrayList,冷门容器却每次面试都问

什么是Fail-Fast机制

Java利用序列化实现对象的深拷贝

一个 ArrayList 就能让你面试到哭!

你可能感兴趣的:(数据结构与算法,java,数据结构)