[java學習]java容器源碼初探(1)
一、動態數組ArrayList
在我們開發人員眼中,這就是一個“動態數組”,可以“動態”地調整數組的大小,雖然說數組從定義了長度後,就不能改變大小。
實現“動態”調整的基本原理就是:按照某個調整策略,重新建立一個調整後一樣大小的數組,然後將原來的數組賦值回去。
下面我們來解析一下幾個與數組不一樣的方法。
看看ArrayList中主要的幾個欄位(源碼剖析):
// 預設的初始數組大小 private static final int DEFAULT_CAPACITY = 10; // 空數組的表示 private static final Object[] EMPTY_ELEMENTDATA = {}; // 資料儲存的數組,主體就是它 private transient Object[] elementData; // 數組元素個數(不一定等於數組的length) private int size;
添加元素(源碼剖析):
public boolean add(E e) { // 判斷數組需不需要擴容,需要就擴容(動態調整數組大小的關鍵方法) ensureCapacityInternal(size + 1); // 數組元素賦值,數組元素個數+1 elementData[size++] = e; return true;}public void add(int index, E element) { // 監測是否是數組越界 rangeCheckForAdd(index); // 判斷數組需不需要擴容,需要就擴容(動態調整數組大小的關鍵方法) ensureCapacityInternal(size + 1); // index和index的元素向後移動 System.arraycopy(elementData, index, elementData, index + 1, size - index); // index位置上賦值 elementData[index] = element; // 數組元素+1 size++;}
刪除元素,這裡注意,並沒有真正的縮小數組的大小,只是修改size的值(源碼剖析):
public E remove(int index) { // 數組越界檢查,會拋出異常哦 rangeCheck(index); // 記錄修改次數,在迭代器那裡會有用 modCount++; // 找到要刪除的資料 E oldValue = elementData(index); // 需要移動的元素個數 int numMoved = size - index - 1; if (numMoved > 0) // 複製數組,移動元素 System.arraycopy(elementData, index+1, elementData, index,numMoved); // 移除引用,方便GC回收 elementData[--size] = null; return oldValue;}public boolean remove(Object o) { if (o == null) { // 遍曆數組,尋找到第一個為null值的元素,然後刪除 for (int index = 0; index < size; index++) if (elementData[index] == null) { // 快速刪除 fastRemove(index); return true; } } else { // 同樣是遍曆數組,尋找 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { // 找到,刪除 fastRemove(index); return true; } } // 找不到 return false;}// 與E remove(int index)的差不多,我就不多說了。// 這裡我就不明白,remove(int index)和fastRemove(int index)明明可以代碼複用,為何不複用代碼呢?private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); // 置null,讓GC可以回收它 elementData[--size] = null;}
看看最重要的數組擴容方法(源碼剖析):
private void ensureCapacityInternal(int minCapacity) { // 判斷數組是否為空白數組 if (elementData == EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { // 修改次數+1 modCount++; // 判斷需不需要擴容:比較數組大小和需要擴容的大小 if (minCapacity - elementData.length > 0) grow(minCapacity); } // 增加容量開始 private void grow(int minCapacity) { int oldCapacity = elementData.length; // x1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) // 擴容的大小太小了 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) // 擴容的大小不會超過最大容量Integer.MAX_VALUE-8 newCapacity = hugeCapacity(minCapacity); // 數組複製 elementData = Arrays.copyOf(elementData, newCapacity); }
ArrayList被稱為“動態數組的原理”,大致就如源碼解析中注釋。裡面還有其他數組操作的方法,這裡就不多講,其實思路也是差不多的,一切以源碼為準。
二、鏈表隊列LinkedList
(雙向)鏈表+隊列(Deque)。以前我一直以為LinkedList只是個鏈表而已,沒想到它可以當隊列用。
看看結點代碼,一看就知道是雙向的結點(源碼):
private static class Node { E item; Node next; // 後繼節點 Node prev; // 前置節點 Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; } }
幾個重要欄位(源碼解析):
// transient:不被序列化 // 結點個數 transient int size = 0; // 第一個結點 transient Node first; // 最後一個結點 transient Node last;
簡單的看看鏈表的代碼(源碼解析):
// 添加結點 public void add(int index, E element) { // 檢查index是否越界 checkPositionIndex(index); if (index == size) // 添加最後一個 linkLast(element); else // 添加到原index結點前面 linkBefore(element, node(index)); } // 刪除結點 public E remove(int index) { // 檢查index是否越界 checkElementIndex(index); // 解除結點連結 return unlink(node(index)); } // 尋找結點 public E get(int index) { // 檢查越界了沒 checkElementIndex(index); // 開始尋找 return node(index).item; } // “真正”是使用這個方法來尋找 Node node(int index) { // 折半尋找~,果然是會利用雙向鏈表的優點 if (index < (size >> 1)) { // 從前面找找前半邊 Node x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { // 從後面找找後半邊 Node x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
簡單的看看隊列的代碼(源碼解析):
// 入隊 public boolean offer(E e) { return add(e); } // 也是入隊 public boolean add(E e) { // 接到最後一個結點 linkLast(e); // 個人覺得這個return true沒必要,完全可以無傳回值void,以為如果入隊失敗linkLast會拋異常。既然是拋異常了,就不會return false了,那就沒必要return true。 return true; } // “溫柔地”取隊頭元素,隊為空白不會拋出異常,只會返回null public E peek() { final Node f = first; return (f == null) ? null : f.item; } // “粗暴地”取隊頭元素,隊為空白拋出NoSuchElementException異常 public E element() { return getFirst(); } // “溫柔地”出隊,隊為空白不會拋出異常,只會返回null public E poll() { final Node f = first; return (f == null) ? null : unlinkFirst(f); } // “粗暴地”出隊,隊為空白拋出NoSuchElementException異常 public E remove() { return removeFirst(); }
三、安全的動態數組Vector
加鎖版的ArrayList(安全執行緒)。方法實現與ArrayList差不多,不同的是線程不安全的方法都加上鎖(synchronized)了,這裡就不講啦。
一般不使用Vector而使用ArrayList,除非在多線程環境下(萬不得已)才使用Vector。
四、棧Stack
棧,父類是Vector,所以你懂的(加鎖版動態數組)。既然父類“安全”了,作為子類的棧也要“安全”了(加鎖!),這麼說棧也不是很快嘛~。
Stack規定數組最後一個元素為棧頂元素。
入棧(源碼解析):
// 不加鎖?父類Vector的addElement已經加鎖了,所以不加多餘的鎖。 public E push(E item) { // 添加元素到數組最後一個元素的後面 addElement(item); // 返回壓入棧的元素 return item; }
出棧(源碼解析):
// 出棧 public synchronized E pop() { E obj; // 棧中元素個數 int len = size(); // 取棧頂元素 obj = peek(); // 移除最後的一個元素(就是移除棧頂元素啦) removeElementAt(len - 1); // 返回移除的棧頂元素 return obj; } // 取棧頂元素 public synchronized E peek() { int len = size(); if (len == 0) // 空棧異常 throw new EmptyStackException(); // 取數組最後一個元素 return elementAt(len - 1); }
棧中元素尋找,從棧頂(最後一個數組元素)到棧底(第一個數組元素)(源碼解析):
public synchronized int search(Object o) { // 找到最後一個符合要求的元素 int i = lastIndexOf(o); if (i >= 0) { // 找到了 return size() - i; } // 沒找到 return -1; }
五、優先隊列PriorityQueue
PriorityQueue實現了優先隊列(預設最小堆)。
實現優先隊列的痛點:需要調整堆。
調整堆的方法有:上濾和下濾。
幾個重要欄位(源碼解析):
// 預設的初始化容器大小 private static final int DEFAULT_INITIAL_CAPACITY = 11; // 元素數組 private transient Object[] queue; // 元素個數 private int size = 0; // 比較子(可拓展),可以為空白,為空白的話E就必須實現Comparable介面。 private final Comparator comparator; // 修改次數 private transient int modCount = 0;
添加元素(入隊)(源碼解析):
public boolean add(E e) { return offer(e); } public boolean offer(E e) { if (e == null) throw new NullPointerException(); // 修改次數+1 modCount++; int i = size; // 判斷是否應該擴充數組 if (i >= queue.length) // 數組擴容 grow(i + 1); // 數組元素增加1 size = i + 1; if (i == 0) // 第一個入隊的元素 queue[0] = e; else // 因為有元素加入,需要調整堆(從下到上):上濾 siftUp(i, e); return true; }
我們來看看調整堆的方法(源碼解析):
上濾(源碼解析):
// 選擇比較子上濾還是Comparable對象的上濾 private void siftUp(int k, E x) { if (comparator != null) // 優先選擇有比較子的進行上濾 siftUpUsingComparator(k, x); else // Comparable對象的上濾 siftUpComparable(k, x); } // Comparable對象的上濾 private void siftUpComparable(int k, E x) { // 強制轉換成Comparable對象,這樣才能有下面的比較 Comparable key = (Comparable) x; while (k > 0) { // k是子結點在數組中的下標,(k-1)/2可以求出父結點下標 int parent = (k - 1) >>> 1; // 取得父結點 Object e = queue[parent]; // 如果key大於或等於父節點 if (key.compareTo((E) e) >= 0) break; // key小於父結點 // 父結點與子結點交換值 queue[k] = e; // k跑到父結點的位置 k = parent; } // 賦值到合適位置 queue[k] = key; } // 使用比較子的上濾(解析和siftUpComparable一樣) private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; }
下濾(源碼解析):
// 下濾(和上濾一樣有兩次方式) private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } // Comparable對象的下濾 private void siftDownComparable(int k, E x) { Comparable key = (Comparable)x; // 計算出非葉子結點個數 int half = size >>> 1; while (k < half) { // 計算出左孩子的位置 int child = (k << 1) + 1; // 取得左孩子的對象 Object c = queue[child]; // 右孩子的位置 int right = child + 1; // 如果右孩子存在,並且左孩子大於右孩子 if (right < size && ((Comparable) c).compareTo((E) queue[right]) > 0) // 選擇右孩子作為與父結點的比較對象 c = queue[child = right]; // 父子結點比較 if (key.compareTo((E) c) <= 0) // 如果父節點小於或等於子結點,則找到要調整的位置 break; // 子結點值賦值到父結點 queue[k] = c; // 換子結點進入下次迴圈的比較 k = child; } // 找到調整的位置,賦值 queue[k] = key; } // 使用比較子的下濾(解析和siftDownComparable一樣) private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; }
總結:閱讀一下java容器包中的類後,對資料結構的瞭解又加深了一層,確實源碼裡的都是“好東西”。這裡的講的類還不全面,未完持續!