ArrayList源碼分析-JDK1.8

1.概述

ArrayList本質上是一個數組,它內部經過對數組的操做實現了List功能,因此ArrayList又被叫作動態數組.每一個ArrayList實例都有容量,會自動擴容.它可添加null,有序可重複,線程不安全.VectorArrayList內部實現基本是一致的,除了Vector添加了synchronized保證其線程安全.java

1.1繼承體系

2.源碼解析

2.1屬性

/**
 * 默認初始容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 初始容量爲0時,elementData指向此對象(空元素對象)
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 調用ArrayList()構造方法時,elementData指向此對象(默認容量空元素對象)
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 用於存儲集合元素,非private類型可以簡化內部類的訪問(在編譯階段)
 */
transient Object[] elementData;

/**
 * 包含的元素個數
 */
private int size;

爲何DEFAULT_CAPACITY這種變量爲何要聲明爲private static final類型數組

private是爲了把變量的做用範圍控制在類中.安全

static修飾的變量是靜態變量.JVM只會爲靜態變量分配一次內存.這樣不管對象被建立多少次,此變量始終指向的都是同一內存地址.達到節省內存,提高性能的目的.性能優化

final修飾的變量在被初始化後,不可再被指向別的內存地址,以防變量的地址被篡改.併發

2.2構造方法

無參構造方法dom

public ArrayList() {
        //無參構造器方法,將elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

指定容量構造方法oop

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //initialCapacity大於0時,將elementData指向新建的initialCapacity大小的數組.
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //initialCapacity爲空時,將elementData指向EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " +
                initialCapacity);
    }
}

指定集合構造方法源碼分析

public ArrayList(Collection<? extends E> c) {
    //將elementData指向c轉換後的數組
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        //c.toArray 可能不會返回Object[],因此須要手動檢查下.關於這點,會單獨講解下,看3.3
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //若是elementData.length爲0,將elementData指向EMPTY_ELEMENTDATA.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList的無參構造方法使用頻率是很是高的,在第一次添加元素時,會將capacity初始化爲10.在咱們知道ArrayList中須要存儲多少元素時,使用指定容量構造方法,可避免擴容帶來的運行開銷,提升程序運行效率.當咱們須要複用Collection對象時,使用指定集合構造方法.性能

2.3插入

2.3.1添加元素到列表尾部

add(E e)源碼測試

//將指定的元素添加到列表的末尾
public boolean add(E e) {
    //確保內部容量,若是容量不夠則計算出所需的容量值.
    ensureCapacityInternal(size + 1);
    //將元素插入到數組尾部,size加一.
    elementData[size++] = e;
    return true;
}

add(E e)方法的平均時間複雜度是O(1).它的流程大致上分爲兩步:

  1. 保證內部容量可用;
  2. 將元素添加到數組尾部;

第一步就是自動擴容機制,具體分析參看2.3.3.

第二步則是在確保有可用容量的基礎上,在尾部添加元素,以下圖:

2.3.2將元素插入到指定位置

add(int index, E element)源碼

//在列表的指定位置上添加指定元素,在添加以前將在此位置上的元素及其後面的元素向右移一位.
public void add(int index, E element) {
    //檢查索引是否越界
    rangeCheckForAdd(index);
    //確保內部容量,若是容量不夠則計算出所需的容量值.
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //將index及index以後的元素向右移一位.
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    //將新元素插入到index處.
    elementData[index] = element;
    //元素個數加一.
    size++;
}

//檢查索引是否越界
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

add(int index, E element)的平均時間複雜度是O(N).因此在大容量的集合中不要頻繁使用此方法,不然可能會產生效率問題.在指定位置添加元素的流程以下圖所示:

2.3.3自動擴容

ensureCapacityInternal(int minCapacity)源碼

//是否須要擴容
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    //計算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //若是elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,返回DEFAULT_CAPACITY和minCapacity中的較大值.
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //不然直接返回minCapacity
        return minCapacity;
    }

    //確保明確的容量
    private void ensureExplicitCapacity(int minCapacity) {
        //修改次數加一
        modCount++;

        //若是minCapacity大於elementData數組長度,那麼進行擴容.
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    //要分配的最大數組大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 增長容量確保數組至少可以容納最小容量參數指定的元素個數.
     *
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        //聲明oldCapacity爲elementData長度
        int oldCapacity = elementData.length;
        //將newCapacity聲明爲oldCapacity的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //若是newCapacity小於minCapacity,將newCapacity指向minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //若是newCapacity超出了最大數組長度,調用hugeCapacity()方法計算newCapacity.
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //根據newCapacity生成一個新的數組,並將elementData老數據放入elementData中.
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    /**
     *
     * @param minCapacity 最小容量
     * @return 計算後的容量
     */
    private static int hugeCapacity(int minCapacity) {
        //若是minCapacity小於0,拋出內存溢出異常.
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //比較得出所需容量
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

自動擴容的流程參看上面代碼,總結幾個要點:

  • 若是使用的是new ArrayList()構造方法,在添加第一個元素時,容量會被設置爲DEFAULT_CAPACITY(10)大小.
  • 容量會被擴容爲以前的1.5倍
  • 若是擴容後的容量大於MAX_ARRAY_SIZE,那麼將Integer.MAX_VALUE做爲容量.

2.4刪除

remove(int index)

//移除指定索引位置元素
    public E remove(int index) {
        //index越界檢查
        rangeCheck(index);

        modCount++;
        //獲取將要刪除的元素
        E oldValue = elementData(index);
        //獲取將要移動的元素個數
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //將index以後的元素向左移一位
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        //size減一,並將elementData數組最後一個元素指向null,讓GC進行操做
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

remove(Object o)

//刪除指定元素
    public boolean remove(Object o) {
        //若是對象爲null,刪除數組中的第一個爲null的元素.沒有null元素的話則不會變化
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            //刪除數組中的第一個爲o的元素,沒有則不操做.
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    //省略了越界檢查,並且不會返回被刪除的值,反映了JDK將性能優化到極致.
    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
    }

刪除操做的平均時間複雜度是O(N),其主要步驟是:

  1. 將索引後面的元素向左移一位;
  2. 數組的最後一個元素賦值爲null;
  3. size減一;

具體步驟以下圖所示:

2.5遍歷

ArrayList實現了RandomAccess接口.RandomAccess是標識接口,標識實現類可以快速隨機訪問存儲元素.由於ArrayList的底層是數組,經過下標index訪問.因此在ArrayList元素量較大時,應當使用普通for循環,也就是經過下標進行訪問.而LinkedList底層是鏈表,不具有快速隨機訪問的能力,所以沒有實現RandomAccess接口,推薦使用forEach遍歷(也就是Iterator遍歷).

測試代碼:

@Test
public void test6() {
    //實現了RandomAccess接口,底層是數組的ArrayList測試
    List<Long> arrayList = new ArrayList<>(150000000);
    Random random = new Random();
    //爲了更好的展現測試結果,避免程序運行時間過長,arrayList和linkedList添加元素的個數不一樣.
    for (int i = 0; i < 100000000; i++) {
        arrayList.add(random.nextLong());
    }
    System.out.println("======ArrayList======");
    traverseByLoop(arrayList);
    traverseByIterator(arrayList);

    System.out.println("======LinkedList======");
    //沒有實現RandomAccess接口,底層是鏈表的LinkedList測試
    LinkedList<Long> linkedList = new LinkedList<>();
    for (int i = 0; i < 100000; i++) {
        linkedList.add(random.nextLong());
    }
    traverseByLoop(linkedList);
    traverseByIterator(linkedList);
}

//普通for循環進行遍歷
public void traverseByLoop(List<Long> list) {
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
        list.get(i);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("RandomAccess遍歷用時:" + (endTime - startTime) + "ms");
}

//Iterator遍歷
public void traverseByIterator(List<Long> list) {
    long startTime = System.currentTimeMillis();
    Iterator<Long> iterator = list.iterator();
    while (iterator.hasNext()) {
        iterator.next();
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Iterator遍歷用時:" + (endTime - startTime) + "ms");
}

運行test6()方法,控制檯輸出:

======ArrayList======
RandomAccess遍歷用時:4ms
Iterator遍歷用時:10ms
======LinkedList======
RandomAccess遍歷用時:5572ms
Iterator遍歷用時:3ms

3.其它細節

3.1fail-fast機制

Iterator被建立後,若是List對象不是調用iteratorremove()add(Object obj)方法更改內部結構.iterator就會拋出ConcurrentModificationException.以此避免在迭代過程當中List對象不可知的變化.這個機制只能用來偵測異常的操做,並不能做爲併發操做的保障.在JDK1.5新增的forEach循環,其本質就是用迭代器遍歷.下面用forEach語法來對fail-fast機制進行測試.

@Test
public void test1() {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
    for (Integer num : list) {
        if (1 == num) {
            list.remove(num);
        }
    }
    System.out.println(list);
}

因爲在遍歷過程當中調用了Listremove()方法,致使程序檢測到非法修改,拋出異常.

3.2fail-fast失效

forEach中使用非iterator方法刪除List的倒數第二個元素,fail-fast不會生效.

@Test
public void test2() {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2));

    for (Integer num : list) {
        if (1 == num) {
            list.remove(num);
        }
    }
    System.out.println(list);
}

forEachjava的語法糖,爲了搞清楚爲啥出現上面的問題,咱們將上面的代碼轉換爲Iterator遍歷.

@Test
public void test3() {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2));

    Iterator<Integer> iterator = list.iterator();

    while (iterator.hasNext()) {
        Integer next = iterator.next();
        if (1 == next) {
            list.remove(next);
        }
    }

    System.out.println(list);
}

首先看看list.iterator()的源碼,看看這個Iterator是啥?

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

Itr類是ArrayList的內部類,咱來看看源碼.

private class Itr implements Iterator<E> {
    //下一個要返回元素的索引,默認爲0.
    int cursor;
    //以前返回元素的索引,默認爲-1.
    int lastRet = -1;
    //保存建立時modCount的值
    int expectedModCount = modCount;

    Itr() {
    }

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

    @SuppressWarnings("unchecked")
    public E next() {
        //fail-fast檢測
        checkForComodification();
        //將i值聲明爲cursor
        int i = cursor;
        //index越界檢查
        if (i >= size)
            throw new NoSuchElementException();

        Object[] elementData = ArrayList.this.elementData;
        //若是i值大於等於當前數組的長度,fail-fast
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        //光標加一
        cursor = i + 1;
        //對lastRet賦值並返回值.
        return (E) elementData[lastRet = i];
    }

    //每次操做時比較expectedModCount和modCount的值,若不一致,拋出ConcurrentModificationException
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

while (iterator.hasNext())開始分析,第一次進入hasNext()方法.cursor爲0,size爲2,返回true,進入循環體.而後進入next()方法,正常執行,cursor變爲1,lastRet變爲0.而後開始執行ArrayListremove()方法,modCount變爲1,size變爲1.這時候再進入第二次循環,執行hasNext()方法,cursor是1,size也是1,返回false,退出循環體.所以fail-fast失效.因此不要在forEach循環中使用非Iterator的方法進行增刪操做,fail-fast也不能徹底避免數據被更改的風險,從源頭規避風險是首選.

3.3c.toArray()返回非Object[]

ArrayList的集合構造器源碼中有c.toArray might (incorrectly) not return Object[]這句註釋.就上網查了下這個問題.咱們先來看下代碼:

@Test
public void test5() {
    //獲取並輸出Object數組類型
    Object[] objArr = new Object[0];
    //class [Ljava.lang.Object;
    System.out.println(objArr.getClass());

    //經過Arrays.asList()方法構建List對象.該對象的class不是Object[]類型.
    List<Integer> list1 = Arrays.asList(1, 2, 3);
    //class [Ljava.lang.Integer;
    System.out.println(list1.toArray().getClass());

    //經過new ArrayList構造器建立List對象
    ArrayList<Integer> list2 = new ArrayList<>(Arrays.asList(4, 5, 6));
    //class [Ljava.lang.Object;
    System.out.println(list2.toArray().getClass());
}

控制檯輸出:

class [Ljava.lang.Object;
class [Ljava.lang.Integer;
class [Ljava.lang.Object;

能夠看到經過Arrays.asList(1, 2, 3)建立的對象class不是Object[]類型.至於具體緣由再也不分析,感興趣的朋友研究下哈.

3.4elementData爲啥定義爲transient

ArrayList本身根據size序列化真實的元素,而不是根據數組的長度序列化元素,減小了空間佔用.

4.參考

田小波-ArrayList 源碼分析

彤哥讀源碼-死磕 java集合之ArrayList源碼分析

IT從業者說-RandomAccess 這個空架子有何用?

相關文章
相關標籤/搜索