1. 程式人生 > >一篇文章帶您讀懂List集合(原始碼分析)

一篇文章帶您讀懂List集合(原始碼分析)

今天要分享的Java集合是List,主要是針對它的常見實現類ArrayList進行講解

內容目錄

什麼是List核心方法原始碼剖析1.文件註釋2.構造方法3.add()3.remove()如何提升ArrayList的效能ArrayList可以代替陣列嗎?

什麼是List

  List集合是線性資料結構的主要實現,用來存放一組資料。我們稱之為:列表。


  ArrayList是List的一個常見實現類,它的面試頻率和使用頻率都非常高,所以我們今天通過學習ArrayList來對Java中的List集合有一個深入的理解。
  ArrayList最大的優勢是可以將陣列的操作細節封裝起來,比如陣列在插入和刪除時要搬移其他資料。另外,它的另一大優勢,就是支援動態擴容,這也是我們使用ArrayList的主要場景之一,在某些情況下我們沒有辦法在程式編譯之前就確定儲存資料
容器的大小。

 

核心方法原始碼剖析

  這一部分,選取了ArrayList的一些核心方法進行講解。分別是:構造方法,add()、和remove()。這裡有一個小竅門,我們在讀jdk原始碼的時候,一定要先看類上的doc註釋,比較核心的知識點都會寫在上面。有一個初步的概念再去看原始碼,就會容易很多。

1.文件註釋

  This class is roughly equivalent to Vector, except that it is unsynchronized.
  大致相當於Vector,不同之處是不同步(執行緒不安全)

  Implements all optional list operations, and permits all elements, including null
  實現所有可選列表操作,並允許所有元素,包括null

  in the face of concurrent modification, the iterator fails quickly and cleanly
  面對併發修改,迭代器將快速而乾淨地失敗

2.構造方法

  ArrayList()提供了三種構造方法。
  ArrayList():構造一個初始容量為10的空列表。

  ArrayList(int initialCapacity):構造具有指定初始容量的空列表。
  ArrayList(Collection c):構造一個包含指定集合的元素的列表,按照它們由集合的迭代器返回的順序。

1/**
2 * Constructs an empty list with an initial capacity of ten.
3 */
4public ArrayList() {
5   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
6}

  這裡的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一個空的部陣列,不設定初始值時,只是引用這個內部陣列。

 1/**
2 * Constructs an empty list with the specified initial capacity.
3 *
4 * @param  initialCapacity  the initial capacity of the list
5 * @throws IllegalArgumentException if the specified initial capacity
6 *         is negative
7 */
8public ArrayList(int initialCapacity) {
9    if (initialCapacity > 0) {
10        this.elementData = new Object[initialCapacity];
11    } else if (initialCapacity == 0) {
12        this.elementData = EMPTY_ELEMENTDATA;
13    } else {
14        throw new IllegalArgumentException("Illegal Capacity: "+
15                                               initialCapacity);
16    }
17}

  這裡的EMPTY_ELEMENTDATA同樣是一個空內部陣列,為了和DEFAULTCAPACITY_EMPTY_ELEMENTDATA做區分,所以沒有使用一個物件。

3.add()

  add方法是ArrayList中的一個核心方法,涉及到內部陣列的擴容。

 1 /**
2  * Appends the specified element to the end of this list.
3  *
4  * @param e element to be appended to this list
5  * @return <tt>true</tt> (as specified by {@link Collection#add})
6  */
7public boolean add(E e) {
8  ensureCapacityInternal(size + 1);  // Increments modCount!!
9  elementData[size++] = e;
10  return true;
11}

  該方法是在集合中追加元素。其中核心方法是ensureCapacityInternal,意思是確定集合內部容量。

 1private void ensureCapacityInternal(int minCapacity) {
2      ensureExplicitCapacity(
3  calculateCapacity(elementData,minCapacity));
4}
5
6private static int calculateCapacity(Object[] elementData, int 
7      minCapacity) {
8  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
9    return Math.max(DEFAULT_CAPACITY, minCapacity);
10  }
11  return minCapacity;
12}

  這裡首先計算了集合的容量,如果這個ArrayList是通過無參構造建立的,那麼比較預設值10,以及傳入的minCapacity,取最大值,這裡可能有的同學會有疑問,為什麼要比較預設值和minCapacity,預設值不是一定大於minCapacity嗎?,這裡是因為ensureCapacityInternal這個方法不僅僅是add()會呼叫,allAll()也會呼叫。

1public boolean addAll(int index, Collection<? extends E> c) {
2        rangeCheckForAdd(index);
3
4        Object[] a = c.toArray();
5        int numNew = a.length;
6        ensureCapacityInternal(size + numNew);  // Increments modCount
7       //省略部分程式碼..
8    }

  這裡如果numNew大於10,那麼預設值就會不夠用。所以才會在calculateCapacity方法中引入一個求最大值的步驟。
  算出集合儲存資料所需的最小空間後,就要考慮,集合原有儲存空間是否夠用,是否需要擴容。

 1private void ensureExplicitCapacity(int minCapacity) {
2    modCount++;
3    // overflow-conscious code
4    if (minCapacity - elementData.length > 0)
5    grow(minCapacity);
6}
7
8/**
9 * Increases the capacity to ensure that it can hold at least the
10 * number of elements specified by the minimum capacity argument.
11 *
12 * @param minCapacity the desired minimum capacity
13 */
14 private void grow(int minCapacity) {
15    // overflow-conscious code
16    int oldCapacity = elementData.length;
17    int newCapacity = oldCapacity + (oldCapacity >> 1);
18    if (newCapacity - minCapacity < 0)
19    newCapacity = minCapacity;
20    if (newCapacity - MAX_ARRAY_SIZE > 0)
21    newCapacity = hugeCapacity(minCapacity);
22    // minCapacity is usually close to size, so this is a win:
23    elementData = Arrays.copyOf(elementData, newCapacity);
24}

這裡我們主要關注4個點:
  1.int newCapacity = oldCapacity + (oldCapacity >> 1);每次擴容是原陣列的1.5倍
  2.擴容也是有限的,存在最大值:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
  3.集合擴容底層呼叫的是:Arrays.copyOf()方法,需要把陣列中的資料複製一份,到新陣列中,而這個方法底層是System.arrayCopy是一個native方法,效率不高。
  4.最重要的一個點:如果我們可以事先估計出資料量,那麼最好給ArrayList一個初始值,這樣可以減少其擴容次數,從而省掉很多次記憶體申請和資料搬移操作。(不指定初始值,至少會執行一次grow方法,用於初始化內部陣列)。

3.remove()

 1/**
2 * Removes the element at the specified position in this list.
3 * Shifts any subsequent elements to the left (subtracts one from their
4 * indices).
5 *
6 * @param index the index of the element to be removed
7 * @return the element that was removed from the list
8 * @throws IndexOutOfBoundsException {@inheritDoc}
9 */
10public E remove(int index) {
11    rangeCheck(index);
12    modCount++;
13    E oldValue = elementData(index);
14
15    int numMoved = size - index - 1;
16    if (numMoved > 0)
17       System.arraycopy(elementData, index+1, elementData, index,
18                             numMoved);
19    elementData[--size] = null; // clear to let GC do its work
20    return oldValue;
21}

  刪除的程式碼因為不涉及到縮容,所以比起add較為簡單,首先會檢查陣列是否下標越界,然後會獲取指定位置的元素,接著進行資料的搬移,將--size位置的元素置成null,讓GC進行回收。最後將目標元素返回即可。
  另外最後我想提出一個比較容易犯的錯誤,集合在遍歷的時候,對其結構進行修改(刪除、新增元素)。舉一個例子:

 1public class Test {
2    public static void main(String[] args) {
3        List<Integer> list = new ArrayList<>();
4        list.add(1);
5        list.add(2);
6        list.add(3);
7        for (Integer i : list) {
8            if(i.equals(1)){
9                list.remove(i);
10            }
11        }
12    }
13}

結果:

1Exception in thread "main" java.util.ConcurrentModificationException
2    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
3    at java.util.ArrayList$Itr.next(ArrayList.java:859)
4    at jialin.li.Test.main(Test.java:12)

  產生問題的原因,其實文件註釋已經給出了明確的結果,即:
  if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own {@link ListIterator#remove() remove} or {@link ListIterator#add(Object) add} methods, the iterator will throw a {@link ConcurrentModificationException}
  如果列表在任何時間從結構上修改建立迭代器之後,以任何方式除非通過迭代器自身remove種或add方法,迭代器都將丟擲一個ConcurrentModificationException。這裡我建議是遍歷的時候,不要對其結構進行修改,而是採用其他方法(打標,或者複製列表)的方式進行。

如何提升ArrayList的效能

  1 給定初值,省掉很多次記憶體申請和資料搬移操作。
  2 對於讀多寫少的場景,可以使用ArrayList替代LinkedList,可以省記憶體,同時CPU快取的利用率也會更高。(陣列儲存的時候,是記憶體是連續的,CPU讀取記憶體資料、記憶體讀取磁碟資料的時候,都不是一條一條讀取,而是一次讀取臨近的一批資料,所以連續的儲存可以讓CPU更有機會一次讀取較多的有效資料)

ArrayList可以代替陣列嗎?

  不可以,任何資料結構都有它存在的場景和意義,集合沒有辦法儲存基本資料型別,只能儲存包裝型別,包裝型別就意味著需要拆箱和裝箱,會有一定的效能消耗,如果對效能要求非常高的系統,或者只需要使用基本型別,那麼就應該去使用陣列而不是集合。同時陣列在表示多維資料的時候,也更加直觀,比如二維 int[][] 、ArrayList<arraylist>。我們使用集合更多的情況是想利用它的擴容特性,以及增刪資料時不會造成空洞。

  最後,期待您的訂閱和點贊,專欄每週都會更新,希望可以和您一起進步,同時也期待您的批評與指正!