一篇文章帶您讀懂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。我們使用集合更多的情況是想利用它的擴容特性,以及增刪數據時不會造成空洞。

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