【java集合類】ArrayList和LinkedList源碼分析(jdk1.8)


前言:

  ArrayList底層是依靠數組實現的,而LinkedList的實現是含前驅后繼節點的雙向列表。平時刷題時會經常使用到這兩個集合類,這兩者的區別在我眼中主要是ArrayList讀取節點平均時間復雜度是O(1)級別的,插入刪除節點是O(n);LinkedList讀取節點時間復雜度是O(n),插入節點是O(1)。

  本文記錄我對jdk1.8下的ArrayList和LinkedList源碼中主要內容的學習。

1、ArrayList

1.1 主要成員變量

 1     //默認容量
 2     private static final int DEFAULT_CAPACITY = 10;
 3     //空的數組
 4     private static final Object[] EMPTY_ELEMENTDATA = {};
 5 
 6     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 7     //數據數組
 8     transient Object[] elementData; // non-private to simplify nested class access
 9     //當前大小
10     private int size;

  主要成員變量如上,最重要的就是size和elementData,其中elementData的修飾transient一開始很令我費解,查閱資料后豁然開朗,transient是為了序列化ArrayList時不用Java自帶的序列化機制,而用ArrayList定義的兩個方法(writeObject、readObject),實現自己可控制的序列化操作,防止數組中大量NULL元素被序列化。

1.2 主要方法

1.2.1 構造方法

  構造方法源碼其實很簡單,不過在此提及是為了給后面擴容引出一個思考。

 1     public ArrayList(int initialCapacity) {
 2         if (initialCapacity > 0) {
 3             this.elementData = new Object[initialCapacity];
 4         } else if (initialCapacity == 0) {
 5             this.elementData = EMPTY_ELEMENTDATA;
 6         } else {
 7             throw new IllegalArgumentException("Illegal Capacity: "+
 8                                                initialCapacity);
 9         }
10     }
11 
12    
13     public ArrayList() {
14         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
15     }

  源碼如上,一個不帶參數的構造器,以及帶容量參數的構造器。

1.2.2 add方法

 1     public boolean add(E e) {
 2         ensureCapacityInternal(size + 1);  // Increments modCount!!
 3         elementData[size++] = e;//加到末尾
 4         return true;
 5     }
 6 
 7     private void ensureCapacityInternal(int minCapacity) {
 8         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 9             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
10         }
11 
12         ensureExplicitCapacity(minCapacity);
13     }
14 
15     //判斷是否需要擴容
16     private void ensureExplicitCapacity(int minCapacity) {
17         modCount++;
18 
19         // overflow-conscious code
20         if (minCapacity - elementData.length > 0)
21             grow(minCapacity);
22     }

  add方法中先用ensureCapacityInternal方法,首先判斷是否位第一次add,也就是初始化。如果數組位空,那么DEFAULT_CAPACITY就是為10。然后判斷是否需要擴容,如果原size+1比數組的length大就需要擴容。(擴容)后把要加的元素加到末尾即可。

1.2.3 擴容方法(grow)

 1     private void grow(int minCapacity) {
 2         // overflow-conscious code
 3         int oldCapacity = elementData.length;
 4         int newCapacity = oldCapacity + (oldCapacity >> 1);
 5         if (newCapacity - minCapacity < 0)
 6             newCapacity = minCapacity;
 7         if (newCapacity - MAX_ARRAY_SIZE > 0)
 8             newCapacity = hugeCapacity(minCapacity);
 9         // minCapacity is usually close to size, so this is a win:
10         elementData = Arrays.copyOf(elementData, newCapacity);
11     }

  擴容方法如上,hugeCapacity判斷minCapacity是否大於ArrayList上限,如果大於就返回ArrayList的容量上限。用Arrays.copyof新生成一個數組,而newCapacity = oldCapacity + (oldCapacity >> 1)則是將容量變為原來的1.5倍。

  因為ArrayList默認初始容量為10,每次擴容將容量變為1.5倍,而如果使用ArrayList時要一次性add100個元素,則會頻繁用調用擴容方法,因此可以在初始化ArrayList時使用帶參的構造函數,定一個合適的容量值。

1.2.4 remove方法

 1     public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5         E oldValue = elementData(index);
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // clear to let GC do its work
12 
13         return oldValue;
14     }
15 
16     public boolean remove(Object o) {
17         if (o == null) {
18             for (int index = 0; index < size; index++)
19                 if (elementData[index] == null) {
20                     fastRemove(index);
21                     return true;
22                 }
23         } else {
24             for (int index = 0; index < size; index++)
25                 if (o.equals(elementData[index])) {
26                     fastRemove(index);
27                     return true;
28                 }
29         }
30         return false;
31     }

  remove方法主要有兩種,一種是根據下標remove,另一種是根據傳入的元素匹配刪除第一個遇到的該元素,值得一提的是可以刪除null元素(總感覺怪怪的)。

2、LinkedList

  LinkedList是一個雙向鏈表,可以當(雙端)隊列用。

2.1 主要成員變量

 1     transient int size = 0;
 2     
 3     //頭節點
 4     transient Node<E> first;
 5 
 6     //尾節點
 7     transient Node<E> last;
 8 
 9     //Node節點
10     private static class Node<E> {
11         E item;
12         Node<E> next;//前驅
13         Node<E> prev;//后繼
14 
15         Node(Node<E> prev, E element, Node<E> next) {
16             this.item = element;
17             this.next = next;
18             this.prev = prev;
19         }
20     }

  帶首尾的雙向列表,加一個size變量記錄當前節點數量,transient修飾和ArrayList中修飾數組的原因是一樣的,同樣實現了writeObject和readObject,自己實現把size和每一個節點都序列化和反序列化了。

2.2 主要方法

2.2.1 add方法

 1     //add方法添加元素到末尾
 2     public boolean add(E e) {
 3         linkLast(e);
 4         return true;
 5     }
 6     
 7     //添加元素至末尾
 8     void linkLast(E e) {
 9         final Node<E> l = last;
10         final Node<E> newNode = new Node<>(l, e, null);//新建元素,把前驅節點置為原來的last節點
11         last = newNode;
12         if (l == null)//如果尾節點是空(說明頭節點也是空的),就把頭節點設置成新節點
13             first = newNode;
14         else//原來尾節點的后繼設置成新節點
15             l.next = newNode;
16         size++;
17         modCount++;
18     }

  一種add就是上面代碼的加到末尾,分析都在注釋中了。另一種則是添加到指定index,add的平均時間復雜度為O(n)。

 1     public void add(int index, E element) {
 2         checkPositionIndex(index);
 3 
 4         if (index == size)
 5             linkLast(element);//index是最后一個就直接插到最后
 6         else
 7             linkBefore(element, node(index));
 8     }
 9 
10     //將節點插入到目標節點前面
11     void linkBefore(E e, Node<E> succ) {
12         // assert succ != null;
13         final Node<E> pred = succ.prev;
14         final Node<E> newNode = new Node<>(pred, e, succ);//將插入節點的前驅設置成目標節點的前驅
15         succ.prev = newNode;
16         if (pred == null)//同linkLast中設置后驅節點為目標節點
17             first = newNode;
18         else
19             pred.next = newNode;
20         size++;
21         modCount++;
22     }

2.2.2 remove方法

  remove方法和add方法類似,由於是雙端隊列,因此需要改變刪除節點的前驅和后繼節點的后繼和前驅。在此不再展開描述。

2.2.3 get方法

  get方法在此不貼源碼了,由於是雙端隊列,因此如果查找的下標大於size的一半,就從后面往前遍歷,雖然時間復雜度還是o(n)級別的,不過也算是一個小優化吧。

 

  本篇簡略的對jdk1.8下的ArrayList和LinkedList源碼實現進行了分析,期間被幾個命名奇怪的方法勾引走了,比如ArrayList的trimToSize,可以將數組多余的(大於size)的部分“刪掉”。也學到了不少(emmm,好像沒有特別多)東西。本篇博客算是對學習過程的一個記錄吧。(才不會說是好久沒更新博客了要懶死了QAQ)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM