前言:
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)。