語言小知識-Java ArrayList類 深度解析

Wizey發表於2019-02-26

花了一天時間,翻譯了一遍 java.util.ArrayList 類的原始碼(1700 多行,還是很有收穫的),包括註釋和程式碼解讀,並提了一些問題,也寫了下自己的理解 點我檢視 ArrayList 原始碼翻譯

  • 問題 1:ArrayList 的 size 和 capacity 怎麼理解?

如果把 ArrayList 看作一個杯子的話,capacity 就是杯子的容積,也就是代表杯子能裝多少東西,而 size 就是杯子裝的東西的體積。杯子可能裝滿了,也可能沒裝滿,所以 capacity >= size 。capacity 過大和過小都不好,過大會造成浪費,過小又存放不下多個元素的值,capacity == size,則 ArrayList 空間利用率最大,但是不利於新增新的元素。當 ArrayList 例項內的元素個數不再改變了,可以使用 trimToSize() 方法最小化 ArrayList 例項來節省空間,也即是使 capacity == size。

  • 問題 2:ArrayList 內部是怎麼存放資料的?

ArrayList 可以看做是陣列的封裝,使用 elementData 陣列來儲存資料,使用 size 來代表 elementData 的非 null 元素個數。elementData 前沒有訪問修飾符,所以只有同類和同包下的類可以直接方法,外界想要知道 ArrayList 例項內元素的個數就要通過 size 屬性。elementData 陣列型別是 Object 型別,可以存放任意的引用型別,不能存放基本的資料型別。

  • 問題 3:ArrayList 類常量 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 怎麼理解?是不是多餘?

這兩個類常量都是空 Object 陣列的引用,都代表 ArrayList 例項的空狀態,也即是 elementData 陣列中還沒有元素。但是 EMPTY_ELEMENTDATA 是使用帶初始化值的構造方法(有參建構函式,一個是指定初始容量,一個是指定初始集合)時使用的,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是使用預設的構造方法,也即是無參的構造方法時使用的。

  • 問題 4:ArrayList 是怎樣實現擴充的?

擴容是發生在新增操作前的,要保證要新增元素在 elementData 陣列中有位置,也即是 size 加上要新增的元素個數要小於 capacity(size + num <= capacity 就說明容量是充足的),所以在新增方法中,先呼叫 ensureCapacityInternal(int) 方法來確保 elementData 容量充足,然後再進行具體的新增操作。如果 ensureCapacityInternal 方法(ensureCapacityInternal 方法中有呼叫了其他方法)發現陣列容量不夠了,就會擴容。擴容實際的方法是 grow(int) 方法,使用位運算子來使陣列的容量擴容 1.5 倍。但是需要注意的是,沒有指定初始化值的 ArrayList 例項,第一次擴容並不是以 1.5 倍擴容的,而是使用的預設容量 10,所以網上很多直接說 ArrayList 擴容是 1.5 倍也有不當之處,這點從 JDK 原始碼中可以很明確的看出來。
如果在構造 ArrayList 例項時,指定初始化值(初始化容量或者集合),那麼就會建立指定大小的 Object 陣列,並把該陣列物件的引用賦值給 elementData;如果不指定初始化值,在第一次新增元素值時會使用預設的容量大小 10 作為 elementData 陣列的初始容量,使用 Arrays.conpyOf() 方法建立一個 Object[10] 陣列。

  • 問題 5:Arrays.copyOf 方法和 System.arraycopy 方法的區別?

Arrays.copyOf(T[], int length) 方法是 Arrays 工具類中用來進行任意型別陣列賦值(包括 null 值),並使陣列具有指定長度的方法,ArrayList 中用這個方法來實現 elementData 陣列的元素移動。但實際上 Arrays.copyOf 方法最終呼叫的是 System.arraycopy(U[], int srcPos, T[], desPos, int length) 方法,這個方法是一個本地方法,不能直接看原始碼。U 和 T 都是一種泛型,只是為了便於區分,U 表示的是原始陣列(源陣列)型別,T 表示的是存放拷貝值的陣列(目標陣列)型別,srcPos 是指原始陣列中的起始位置(從原始陣列的哪個位置開始拷貝),desPos 是指存放拷貝值的陣列拷貝起始位置(從目標陣列的哪個位置插入這些拷貝的值),length 表示要拷貝的元素數量(要從原始陣列中拷貝多少個)。

  • 問題 6:ArrayList 的 add 操作優化?

核心就是避免 ArrayList 內部進行擴容。
1、對於普通少量的 add 操作,如果插入元素的個數已知,最好使用帶初始化引數的構造方法,避免 ArrayList 內部再進行擴容,提高效能。
2、對於大量的 add 操作,最好先使用 ensureCapacity 方法來確保 elementData 陣列中有充足的容量來存放我們後面 add 操作的元素,避免 ArrayList 例項內部進行擴容。上面提到的 ensureCapacityInternal 方法是一個私有方法,不能直接呼叫,而 ensureCapacity 方法是一個共有方法,專門提供給開發者使用的,提高大量 add 操作的效能。

測試程式碼如下:

    ArrayList<Integer> list1 = new ArrayList<>();
    int addCount = 1_000_000; // 這個值不要太小,否則效果不明顯
    // 沒有優化
    long begin1 = System.currentTimeMillis();
    for (int i = 0; i < addCount; i++) {
        list1.add(i);
    }
    long end1 = System.currentTimeMillis();
    long cost1 = end1 - begin1;

    ArrayList<Integer> list2 = new ArrayList<>();
    // 有優化
    list2.ensureCapacity(addCount);
    long begin2 = System.currentTimeMillis();
    for (int i = 0; i < addCount; i++) {
        list2.add(i);
    }
    long end2 = System.currentTimeMillis();
    long cost2 = end2 - begin2;

    System.err.println("大量 add 操作沒有優化前的時間花費:" + cost1);
    System.err.println("大量 add 操作優化後的時間花費:" + cost2);
複製程式碼
  • 問題 7:subList 方法生成的子列表會對父列表產生影響嗎?

會,不管使修改子列表的值還是修改父列表的值都會對雙方產生影響。閱讀原始碼,就會發現,subList 方法後的子列表對元素的操作實際上呼叫的還是父列表中對應的方法。

  • 問題 8:ArrayList 中既有 Itr 迭代器類,又有 ListItr 迭代器類,該用哪個?

List 集合可以看作是陣列的包裝型別,遍歷並不像陣列那樣方便,迭代器是為了迭代集合中的元素而存在的。Itr 迭代器類實現了 Iterator 介面,ListItr 迭代器類繼承 Itr 迭代器類,並且實現了 ListIterator 介面,所以 ListItr 類的功能比 Itr 類更強大。Itr 類在迭代過程中不能修改 List 的結構(如 add 操作),否則會丟擲併發修改異常 ConcurrentModificationException,並且在 next 方法之後才能 remove 元素,而 ListItr 類還支援在迭代過程中新增元素,對於 List 集合元素操作更加友好。所以對於 List 集合迭代,最好使用 ListItr 迭代器類。

本文轉自我的微信公眾號《程式設計心路》,檢視 原文連結

相關文章